Compare commits

...
33 Commits
Author SHA1 Message Date
peace-maker 8bc6f6cd2b Merge pull request #2796 from peace-maker/stable_backports
Backport bugfixes from 5.0 to stable
2026-11-08 08:49:07 +01:00
peace-maker 455c8c02c3 Backport: Test on Python 3.15 (#2792)
* Test on Python 3.15

* Use `RLIM_INFINITY` instead of `-1` constant for rlimit modification

Python 3.15 warns against using negative values instead of the constant:

DeprecationWarning: Use RLIM_INFINITY instead of negative limit value.

It has been available since Python 2.

* Update CHANGELOG
2026-10-30 20:49:30 +01:00
Carlos Luna-Pena dbc618de0d Backport: fix #2737: pwnlib.shellcraft submodule imports on python 3.12+ (#2739)
* shellcraft: migrate lazy importer to find_spec for py3.12+

* shellcraft: changelog entry + mypy baseline sync for #2739
2026-10-30 20:49:06 +01:00
peace-maker 34b8345d1d Backport: Support paginated libc.rip API requests (#2767)
* Support paginated libc.rip API requests

libc.rip used to truncate the result set but supports pagination now. Fetch all results reliably now by requesting all pages.

Fixes #2651

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
Peace-Maker 771c7f81ac Backport: Skip ssh.ibt doctest since some runners appear to support it now 2026-10-30 20:49:05 +01:00
Taran 8d69dc6142 Backport: fix(srop): correct amd64 SigreturnFrame uc_sigmask offset (#2762)
* fix(srop): correct amd64 SigreturnFrame uc_sigmask offset

* changelog: add entry for #2762
2026-10-30 20:49:05 +01:00
peace-maker 3911a6a12e Backport: Fix libcdb CI cache not forwarding URI path (#2749)
The nginx docs say that you have to specify the $request_uri manually when a variable is used in the `proxy_pass` value.
Since we just put `/` at the end, all requests were rewritten to `/` instead of the intended e.g. `/api/find` endpoints.

https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

This broke in #2718 but didn't manifest right away since the responses were still cached in CI. Now after the cache expired the fetch failed.
2026-10-30 20:49:05 +01:00
ChrisJr404andChris a5f3873194 Backport: safeeval: allow LIST_APPEND and SET_ADD opcodes (Python 3.14) (#2722)
* safeeval: allow LIST_APPEND and SET_ADD opcodes (Python 3.14)

* changelog: add entry for #2722 (safeeval Python 3.14 opcodes)

* ci: retrigger after transient libcdb network failure

---------

Co-authored-by: Chris (ChrisJr404) <[email protected]>
2026-10-30 20:49:05 +01:00
peace-maker 481704bfd8 Backport: ssh: Discard stderr of lsb_release for Distro lookup (#2704)
* ssh: Discard stderr of `lsb_release`

The "No LSB modules are available." spam interferes with parsing the output for the initial checksec distro parsing when connecting to a new host.

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
zackary 5196a1a0f0 Backport: fix: delay upstream DNS resolution to prevent startup failures (#2718)
Use nginx variable pattern (set $upstream + proxy_pass https://$upstream)
to defer DNS resolution until first request instead of at config parse time.

This fixes the occasional 'host not found in upstream' error in CI when
nginx container starts before Docker DNS is fully available.

Fixes #2712
2026-10-30 20:49:05 +01:00
Peace-Maker be705326a7 Backport: libcdb CI cache: Resolve upstream hostnames on request
Instead of trying to resolve all of them when nginx starts and exit
if any of them fail to resolve.

[emerg] 1#1: host not found in upstream "debuginfod.elfutils.org" in /etc/nginx/nginx.conf:23
2026-10-30 20:49:05 +01:00
Peace-Maker c7e6ef37c8 Backport: Cache gitlab.com libcdb responses in CI
The reponses set a cloudflare `_cfuvid` cookie and had a cache time of 60 seconds.
nginx doesn't cache responses with a `Set-Cookie` header by default,
so ignore the cookie and override the cache time to 12 weeks.

Fixes #2494
2026-10-30 20:49:05 +01:00
Arusekk 210faac75b Backport: no atexit, OS will suffice 2026-10-30 20:49:05 +01:00
peace-maker 8e0e4c6c90 Backport: Close SSH client connection when authentication failed (#2688)
* Close SSH client when authentication failed

The TCP connection stayed open after an exception was thrown
during authentication, even when a `with`-statement guarding the call.
Close the connection before re-raising the exception.

Fixes #2678

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
peace-maker bbac6660b2 Backport: Fix tty and raw arguments in ssh.process (#2699)
* Fix `tty` and `raw` arguments in ssh.process

The arguments were unused and not passed through to the underlying `ssh_process` class.

Fixes #2038

* Add tty tests

* Update CHANGELOG

* Fix buffered process environment output without tty
2026-10-30 20:49:05 +01:00
Rin 2a986f12c1 Backport: refactor: replace unsafe eval with ast.literal_eval in ROP cache loading (#2677)
* refactor: replace unsafe eval with ast.literal_eval in ROP cache loading

* docs: add PR link to CHANGELOG reference section

* docs: restore full CHANGELOG and correctly add PR #2677 reference
2026-10-30 20:49:05 +01:00
peace-maker b60ed9928d Backport: Fix typo causing doctests not getting skipped (#2681)
Downloading all those libcs slowed down the CI a lot.
2026-10-30 20:49:05 +01:00
peace-maker b1391984d1 Backport: asm: try native binutils before fallback architectures (#2669)
* asm: try native binutils before fallback architectures

Some binutils builds support multiple architectures,
like the x86_64 assembler supports i386 too in most
package builds and vice versa. We support this by trying
the other binutils as well when looking for the right tool.
That support is optional though and there are packages
that don't include the "cross" compilation.

When assembling for the host's native architecture, we
still prefer the explicitly named `x86_64-linux-gnu-as`
instead of the native `as` binary. That native binary is
tried as a last resort after looking for any other explictly
named binary. If the fallback package is installed but
compiled without the cross-compiler support, we'd still
find the `i386-as` binary while looking for `x86_64-as`
before trying the native `as` binary.
That would fail to assemble.

This patch moves the native `as` binary forward in the list
to try it before checking the other fallback architecture ones.

Fixes #2509

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
Th3S dd747e1062 Backport: fix(libcdb-cli): return early if no matched libc found (#2646)
* Fix: return early if no matched libc found

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
peace-maker 78fccc055f Backport: Remove "pip" as explicit dependency (#2642)
It looks like this was done to work around a bug in an old pip version years ago.
It caused problems when freezing dependencies which unnecessarily forced pip to the version used at freeze time when installing again.

Fixes #2625
2026-10-30 20:49:05 +01:00
OBarronCS 4f50b45586 Backport: Add aarch64_be qemu mapping (#2653) 2026-10-30 20:49:05 +01:00
a1c7d07db8 Backport: GitHub Actions: Test on Python 3.14 (#2627)
* GitHub Actions: Test on Python 3.14

Python v3.14 -- October 7th
* https://www.python.org/download/pre-releases
* https://www.python.org/downloads/release/python-3140rc3
* https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#allow-pre-releases

* Allow new LOAD_SMALL_INT opcode in safeeval

* Support lambdas in mbruteforce

* Drop pwnlib.util.iters.lookahead

---------

Co-authored-by: Peace-Maker <[email protected]>
Co-authored-by: Arusekk <[email protected]>
2026-10-30 20:49:05 +01:00
Arusekk 6691e8476a Backport: template: error message and fallback on no docker 2026-10-30 20:49:05 +01:00
peace-maker a8bcdf6f8d Backport: Fix log.progress ignoring context.log_console (#2610)
* Fix `log.progress` ignoring `context.log_console`

When setting `context.log_console` to something other than stdout, spinners while using `log.progress` would still be printed to stdout.
Redirect them to the stream specified by `context.log_console` and don't animate if it's not a tty.

Fixes #1702

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
k4lizen bf5e4df220 Backport: Fix aarch64 ABI (#2598)
* upd aarch64 abi

* changelog
2026-10-30 20:49:05 +01:00
Shordenandpeace-maker 68b5bbf16b Backport: Support context.timeout in ~/.pwn.conf (#2592)
* Add float to supported config types

This change functionally just allows setting `context.timeout` in
~/.pwn.conf.

* update changelog

---------

Co-authored-by: peace-maker <[email protected]>
2026-10-30 20:49:05 +01:00
Arusekk 9a7e197754 Backport: Fix double exception on gaierror
Closes #2581
2026-10-30 20:49:05 +01:00
Zach Hanson 96c3ede156 Backport: Fix a bug that incorrectly reports GNU_STACK missing when its not. (#2493) 2026-10-30 20:49:05 +01:00
RocketDev f2eba7c8fd Backport: ROP: fix ROP(ELF(exe)).leave is None in some ELF (#2506)
* ROP: fix `self.leave is None` in some libc

When `pop rbp; pop rsp; ret;` exists in libc, it is chosen instead of
`leave`. Add a specific `order` to filter out the former one.

* add changelog for #2506

* ROP: add sp_move when `pop rsp`

* CHANGELOG: mv #2506 to `5.0.0`
2026-10-30 20:49:05 +01:00
peace-maker 1c7d917432 Backport: CI: Test on Python 3.13 (#2534) 2026-10-30 20:49:05 +01:00
tesujiandpeace-maker 1553670f9d Backport: checksec: Do NOT error when passing directory arguments to commandline tool (#2530)
I often use `checksec *` to lazily avoid typing filenames in a directory.
If the directory contains any other sub-dirs, the command fails.
With this patch, checksec will silently skip dir paths. There's still
TOCTOU issue but I don't think checksec do anything important enough
to explicitly use try/catch to account for that.

Co-authored-by: peace-maker <[email protected]>
2026-10-30 20:49:05 +01:00
peace-maker 8f67b7b7b5 Backport: Raise EOFError during process.recv when stdout closes on Windows (#2524)
* Windows: Raise EOFError during process.recv when stdout closes

If the receiving thread terminates while we're waiting for data to arrive we'd be waiting endlessly. Raise EOFError when .recv()ing on a process with stdout closed and no more data queued up from the receiver thread.

* Update CHANGELOG
2026-10-30 20:49:05 +01:00
Rahulandrahul05ranjan 459dfd7e35 fix(regsort): drop self-assignments through register aliases (#2795)
* fix(regsort): drop self-assignments through register aliases

The self-assignment drop compared register names only (k != v), so an
assignment through an alias (e.g. {'ax': 'eax'} with registers.native32)
was not recognized as a no-op.  It then surfaced as a spurious
1-register dependency cycle, which either raised PwnlibException with
xchg=False or emitted a useless 'mov' of a register into its own alias.

With xchg=False and other registers present, the bogus cycle also
forced the temporary-register code path, reordering unrelated moves and
silently leaving registers with wrong values (see the linked issue).

Compare the mapped names instead, like the rest of the function.

Fixes #2794

Co-authored-by: rahul05ranjan <[email protected]>

* docs(changelog): add regsort register-alias self-assignment fix entry

---------

Co-authored-by: Rahul Ranjan <[email protected]>
2026-10-30 16:20:25 +01:00
26 changed files with 287 additions and 123 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ jobs:
test:
strategy:
matrix:
python_version: ['3.10', '3.12']
python_version: ['3.10', '3.12', '3.13', '3.14', '3.15']
os: [ubuntu-latest]
include:
- python_version: '2.7'
+40 -12
View File
@@ -75,18 +75,14 @@ The table below shows which release corresponds to each branch, and what date th
## 5.0.0 (`dev`)
- [#2598][2598] aarch64: Fix ABI definition
- [#2419][2419] riscv: avoid compressed instructions (if you need compressed, use .option rvc)
- [#2551][2551] Detect when kitty is being used as terminal
- [#2519][2519] Drop Python 2.7 support / Require Python 3.10
- [#2507][2507] Add `+LINUX` and `+WINDOWS` doctest options and start proper testing on Windows
- [#2522][2522] Support starting a kitty debugging window with the 'kitten' command
- [#2524][2524] Raise EOFError during `process.recv` when stdout closes on Windows
- [#2526][2526] Properly make use of extra arguments in `packing` utilities. `sign` parameter requires keyword syntax to specify it.
- [#2517][2517] Allow to passthru kwargs on `ssh.__getattr__` convenience function to fix SSH motd problems
- [#2530][2530] Do NOT error when passing directory arguments in `checksec` commandline tool.
- [#2529][2529] Add LoongArch64 support
- [#2506][2506] ROP: fix `ROP(ELF(exe)).leave` is `None` in some ELF
- [#2504][2504] doc: add example case for `tuple` (host, port pair) in `gdb.attach`
- [#2546][2546] ssh: Allow passing disabled_algorithms keyword argument from ssh to paramiko
- [#2538][2538] Add `ssh -L` / `ssh.connect_remote()` workaround when `AllowTcpForwarding` is disabled
@@ -96,24 +92,18 @@ The table below shows which release corresponds to each branch, and what date th
- [#2590][2590] Add support for finding corefiles under WSL2
- [#2496][2496] Add linux ko file search support
- [#2542][2542] Decode `_IO_*` flags in `FileStructure` member
- [#2592][2592] pwnlib.config: Fix customization of `context.timeout`
- [#2608][2608] Abort on `libcdb file libc.so --unstrip` if eu-unstrip is not installed
- [#2611][2611] Cleanup `pwnlib.lexer` exports and imports
- [#2610][2610] Fix `log.progress` ignoring `context.log_console`
- [#2615][2615] tube/process: Fix redirecting stderr to stdout on Windows
[2598]: https://github.com/Gallopsled/pwntools/pull/2598
[2419]: https://github.com/Gallopsled/pwntools/pull/2419
[2551]: https://github.com/Gallopsled/pwntools/pull/2551
[2519]: https://github.com/Gallopsled/pwntools/pull/2519
[2507]: https://github.com/Gallopsled/pwntools/pull/2507
[2522]: https://github.com/Gallopsled/pwntools/pull/2522
[2524]: https://github.com/Gallopsled/pwntools/pull/2524
[2526]: https://github.com/Gallopsled/pwntools/pull/2526
[2517]: https://github.com/Gallopsled/pwntools/pull/2517
[2530]: https://github.com/Gallopsled/pwntools/pull/2530
[2529]: https://github.com/Gallopsled/pwntools/pull/2529
[2506]: https://github.com/Gallopsled/pwntools/pull/2506
[2504]: https://github.com/Gallopsled/pwntools/pull/2504
[2546]: https://github.com/Gallopsled/pwntools/pull/2546
[2538]: https://github.com/Gallopsled/pwntools/pull/2538
@@ -123,10 +113,8 @@ The table below shows which release corresponds to each branch, and what date th
[2590]: https://github.com/Gallopsled/pwntools/pull/2590
[2496]: https://github.com/Gallopsled/pwntools/pull/2496
[2542]: https://github.com/Gallopsled/pwntools/pull/2542
[2592]: https://github.com/Gallopsled/pwntools/pull/2592
[2608]: https://github.com/Gallopsled/pwntools/pull/2608
[2611]: https://github.com/Gallopsled/pwntools/pull/2611
[2610]: https://github.com/Gallopsled/pwntools/pull/2610
[2615]: https://github.com/Gallopsled/pwntools/pull/2615
## 4.15.1
@@ -137,6 +125,26 @@ The table below shows which release corresponds to each branch, and what date th
- [#2773][2773] Disable debuginfod doctests in libcdb.unstrip_libc
- [#2772][2772] fix(packing): `dd()` no longer crashes on a `bytearray` destination
- [#2787][2787] gdb.corefile: Run gdb directly to prevent detach race
- [#2795][2795] regsort: Fix self-assignment detection through register aliases
- [#2524][2524] Raise EOFError during `process.recv` when stdout closes on Windows
- [#2530][2530] Do NOT error when passing directory arguments in `checksec` commandline tool.
- [#2506][2506] ROP: fix `ROP(ELF(exe)).leave` is `None` in some ELF
- [#2592][2592] pwnlib.config: Fix customization of `context.timeout`
- [#2598][2598] aarch64: Fix ABI definition
- [#2610][2610] Fix `log.progress` ignoring `context.log_console`
- [#2627][2627] Remove `pwnlib.util.iters.lookahead` (broken anyway, fails on Python 3.14+)
- [#2653][2653] Add aarch64_be qemu mapping
- [#2642][2642] Remove `pip` as explicit dependency
- [#2646][2646] fix(libcdb-cli): return early if no matched libc found
- [#2669][2669] asm: try native binutils before fallback architectures
- [#2677][2677] refactor: replace unsafe eval with safeeval.const in ROP cache loading
- [#2699][2699] Fix `tty` and `raw` arguments in `ssh.process()`
- [#2688][2688] Close SSH client connection when authentication failed
- [#2704][2704] ssh: Fix distro lookup on Ubuntu 24.04+
- [#2762][2762] fix(srop): correct amd64 SigreturnFrame `uc_sigmask` offset
- [#2767][2767] Support paginated libc.rip API requests to include all search results
- [#2739][2739] shellcraft: migrate lazy importer to find_spec for Python 3.12+
- [#2792][2792] Fix deprecation warning for not using `RLIM_INFINITY` in `resource.setrlimit` (Python 3.15)
[2694]: https://github.com/Gallopsled/pwntools/pull/2694
[2701]: https://github.com/Gallopsled/pwntools/pull/2701
@@ -144,6 +152,26 @@ The table below shows which release corresponds to each branch, and what date th
[2773]: https://github.com/Gallopsled/pwntools/pull/2773
[2772]: https://github.com/Gallopsled/pwntools/pull/2772
[2787]: https://github.com/Gallopsled/pwntools/pull/2787
[2795]: https://github.com/Gallopsled/pwntools/pull/2795
[2524]: https://github.com/Gallopsled/pwntools/pull/2524
[2530]: https://github.com/Gallopsled/pwntools/pull/2530
[2506]: https://github.com/Gallopsled/pwntools/pull/2506
[2592]: https://github.com/Gallopsled/pwntools/pull/2592
[2598]: https://github.com/Gallopsled/pwntools/pull/2598
[2610]: https://github.com/Gallopsled/pwntools/pull/2610
[2627]: https://github.com/Gallopsled/pwntools/pull/2627
[2653]: https://github.com/Gallopsled/pwntools/pull/2653
[2642]: https://github.com/Gallopsled/pwntools/pull/2642
[2646]: https://github.com/Gallopsled/pwntools/pull/2646
[2669]: https://github.com/Gallopsled/pwntools/pull/2669
[2677]: https://github.com/Gallopsled/pwntools/pull/2677
[2699]: https://github.com/Gallopsled/pwntools/pull/2699
[2688]: https://github.com/Gallopsled/pwntools/pull/2688
[2704]: https://github.com/Gallopsled/pwntools/pull/2704
[2762]: https://github.com/Gallopsled/pwntools/pull/2762
[2767]: https://github.com/Gallopsled/pwntools/pull/2767
[2739]: https://github.com/Gallopsled/pwntools/pull/2739
[2792]: https://github.com/Gallopsled/pwntools/pull/2792
## 4.15.0 (`stable`)
-1
View File
@@ -8,7 +8,6 @@ intervaltree
isort
mako>=1.0.0
paramiko>=1.15.2
pip>=6.0.8
pyelftools>=0.29, <0.30; python_version<'3'
pyelftools>=0.29; python_version>='3'
pygments>=2.0
+1 -1
View File
@@ -142,7 +142,7 @@ class SigreturnABI(SyscallABI):
linux_i386 = ABI('esp', [], 4, 0)
linux_amd64 = ABI('rsp', ['rdi','rsi','rdx','rcx','r8','r9'], 8, 0)
linux_arm = ABI('sp', ['r0', 'r1', 'r2', 'r3'], 8, 0)
linux_aarch64 = ABI('sp', ['x0', 'x1', 'x2', 'x3'], 16, 0)
linux_aarch64 = ABI('sp', ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'], 16, 0)
linux_mips = ABI('$sp', ['$a0','$a1','$a2','$a3'], 4, 0)
linux_ppc = ABI('sp', ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'], 4, 0)
linux_ppc64 = ABI('sp', ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'], 8, 0)
+16 -7
View File
@@ -188,29 +188,36 @@ def which_binutils(util, check_version=False):
# Fix up pwntools vs Debian triplet naming, and account
# for 'thumb' being its own pwntools architecture.
arches = [arch] + {
aliases = {
'thumb': ['arm', 'aarch64'],
'i386': ['x86_64', 'amd64'],
'i686': ['x86_64', 'amd64'],
'amd64': ['x86_64', 'i386'],
'amd64': ['x86_64'],
}.get(arch, [])
# Some binutils can support multiple architectures. Try them as fallbacks.
fallback_arches = {
'i386': ['x86_64', 'amd64'],
'amd64': ['i386'],
'arm': ['aarch64'],
'mips': ['mipsel'],
'mipsel': ['mips'],
'mips64': ['mips', 'mipsel'],
'mips64el': ['mipsel', 'mips'],
'mips64': ['mips64el', 'mips', 'mipsel'],
'mips64el': ['mips64', 'mipsel', 'mips'],
'powerpc64': ['powerpc'],
'sparc64': ['sparc'],
'riscv32': ['riscv64', 'riscv'],
'riscv64': ['riscv32', 'riscv'],
}.get(arch, [])
arches = [arch] + aliases + fallback_arches
# If one of the candidate architectures matches the native
# architecture, use that as a last resort.
# architecture, use that as a last resort before trying fallbacks.
machine = platform.machine()
machine = 'i386' if machine == 'i686' else machine
try:
with context.local(arch = machine):
if context.arch in arches:
arches.append(None)
arches = [arch] + aliases + [None] + fallback_arches
except AttributeError:
log.warn_once("Your local binutils won't be used because architecture %r is not supported." % machine)
@@ -223,6 +230,8 @@ def which_binutils(util, check_version=False):
if platform.system() == 'Windows':
utils = [util + '.exe']
# Try the explicit tools for the target architecture first,
# then the native one optionally, then fallbacks.
for arch in arches:
for gutil in utils:
# e.g. objdump
+3 -5
View File
@@ -15,7 +15,6 @@ parser = common.parser_commands.add_parser(
parser.add_argument(
'elf',
nargs='*',
type=argparse.FileType('rb'),
help='Files to check'
)
parser.add_argument(
@@ -23,12 +22,11 @@ parser.add_argument(
nargs='*',
dest='elf2',
metavar='elf',
type=argparse.FileType('rb'),
help='File to check (for compatibility with checksec.sh)'
)
def main(args):
files = args.elf or args.elf2 or []
files = args.elf or args.elf2 or []
if not files:
parser.print_usage()
@@ -36,9 +34,9 @@ def main(args):
for f in files:
try:
e = ELF(f.name)
e = ELF(f)
except Exception as e:
print("{name}: {error}".format(name=f.name, error=e))
print("{name}: {error}".format(name=f, error=e))
if __name__ == '__main__':
common.main(__file__, main)
+3
View File
@@ -240,6 +240,9 @@ def main(args):
symbols = {pairs[i]:pairs[i+1] for i in range(0, len(pairs), 2)}
matched_libcs = libcdb.search_by_symbol_offsets(symbols, offline_only=args.offline_only, return_raw=True)
if not matched_libcs:
return
for libc in matched_libcs:
print_libc_info(libc)
if args.download_libc:
+4 -3
View File
@@ -89,7 +89,7 @@ def get_docker_image_libraries():
if not (libc and ld):
progress.failure("Could not find libraries")
return None, None
progress.status("Copying libraries to current directory")
for filename, basename in zip((libc, ld), (libc_basename, ld_basename)):
cat_command = ["-c", "chroot %s /bin/sh -c '/bin/cat %s'" % (chroot_dir, filename)]
@@ -108,8 +108,9 @@ def get_docker_image_libraries():
write(basename, contents)
except subprocess.CalledProcessError as e:
print(e.stderr.decode())
log.error("docker failed with status: %d" % e.returncode)
print(e.stderr.decode(), file=sys.stderr)
progress.failure("docker failed with status: %d" % e.returncode)
return None, None
progress.success("Retrieved libraries from Docker image")
return libc_basename, ld_basename
+1 -1
View File
@@ -1748,7 +1748,7 @@ def update_context_defaults(section):
default = ContextType.defaults[key]
if isinstance(default, six.string_types + six.integer_types + (tuple, list, dict)):
if isinstance(default, six.string_types + six.integer_types + (float, tuple, list, dict)):
value = safeeval.expr(value)
else:
log.warn("Unsupported configuration option %r in section %r" % (key, 'context'))
+1 -1
View File
@@ -2123,7 +2123,7 @@ class ELF(ELFFile):
"NX:".ljust(12) + {
True: green("NX enabled"),
False: red("NX disabled"),
None: yellow("NX unknown - GNU_STACK missing"),
None: yellow("NX enabled on new kernels"),
}[self.nx],
"PIE:".ljust(12) + {
True: green("PIE enabled"),
+27 -11
View File
@@ -119,16 +119,32 @@ def query_libc_rip(params):
import requests
url = "{}/api/find".format(LIBC_RIP_URL)
try:
result = requests.post(url, json=params, timeout=20)
result.raise_for_status()
if result.status_code != 200:
log.debug("Error: %s", result.text)
# Handle pagination transparently and fetch all results
offset = 0
count = 1
results = []
while offset < count:
try:
pagination_params = {'offset': offset, 'limit': 100}
log.debug("Querying libc.rip at %s (pagination %s) with parameters: %s", url, pagination_params, params)
result = requests.post(url, params=pagination_params, json=params, timeout=20)
result.raise_for_status()
if result.status_code != 200:
log.debug("Error: %s", result.text)
return None
imm_results = result.json()
# non-paginated result. old libc-database API?
if not isinstance(imm_results, dict):
return imm_results
results.extend(imm_results.get('results', []))
count = imm_results.get('total', 0)
offset += imm_results['count']
if not imm_results['has_more']:
break
except requests.RequestException as e:
log.warn_once("Failed to fetch libc info from libc.rip: %s", e)
return None
return result.json()
except requests.RequestException as e:
log.warn_once("Failed to fetch libc info from libc.rip: %s", e)
return None
return results
# https://libc.rip/
def provider_libc_rip(search_target, search_type):
@@ -697,8 +713,8 @@ def search_by_symbol_offsets(symbols, select_index=None, unstrip=True, offline_o
>>> matched_libcs = search_by_symbol_offsets({'__libc_start_main_ret': '7f89ad926550'}, return_as_list=True)
>>> len(matched_libcs) > 1
True
>>> for buildid in matched_libcs: # doctest +SKIP
... libc = ELF(search_by_build_id(buildid)) # doctest +SKIP
>>> for buildid in matched_libcs: # doctest: +SKIP
... libc = ELF(search_by_build_id(buildid)) # doctest: +SKIP
"""
assert search_type in TYPES, search_type
+2 -1
View File
@@ -546,7 +546,8 @@ class Handler(logging.StreamHandler):
# if the record originates from a `Progress` object and term handling
# is enabled we can have animated spinners! so check that
if progress is None or not term.term_mode:
# Don't show spinners if the output is being redirected
if progress is None or not term.term_mode or not os.path.sameopenfile(context.log_console.fileno(), term.term.fd.fileno()):
super(Handler, self).emit(record)
return
+1
View File
@@ -105,6 +105,7 @@ def archname():
('powerpc64', 'little'): 'ppc64le',
('thumb', 'little'): 'arm',
('thumb', 'big'): 'armeb',
('aarch64', 'big'): 'aarch64_be',
}.get((context.arch, context.endian), context.arch)
@LocalContext
+12 -1
View File
@@ -190,6 +190,15 @@ def regsort(in_out, all_regs, mapping = None, tmp = None, xchg = True, randomize
>>> regsort({'a': 1, 'b': 2}, R)
[('mov', 'a', 1), ('mov', 'b', 2)]
>>> M = {'ax': 'eax', 'eax': 'eax', 'ebx': 'ebx'}
Registers which are assigned one of their own aliases
are dropped, like same-name self-assignments.
>>> regsort({'ax': 'eax'}, M)
[]
>>> regsort({'ax': 'eax'}, M, xchg=False)
[]
>>> regsort({'a': 'b', 'b': 'a'}, R)
[('xchg', 'a', 'b')]
>>> regsort({'a': 'b', 'b': 'a'}, R, tmp='X') #doctest: +NORMALIZE_WHITESPACE
@@ -253,7 +262,9 @@ def regsort(in_out, all_regs, mapping = None, tmp = None, xchg = True, randomize
# Drop all registers which will be set to themselves.
#
# For example, {'eax': 'eax'}
in_out = {k:v for k,v in in_out.items() if k != v}
# This must go through the mapping, so that {'ax': 'eax'} is
# recognized as a self-assignment as well.
in_out = {k:v for k,v in in_out.items() if mapping.get(k, k) != mapping.get(v, v)}
# Collapse constant values
#
+12 -6
View File
@@ -390,6 +390,7 @@ from pwnlib.rop.call import Unresolved
from pwnlib.rop.gadgets import Gadget
from pwnlib.util import lists
from pwnlib.util import packing
from pwnlib.util import safeeval
from pwnlib.util.cyclic import cyclic
from pwnlib.util.packing import pack
from pwnlib.util.misc import python_2_bytes_compatible
@@ -1272,7 +1273,7 @@ class ROP(object):
filename = self.__get_cachefile_name(elf)
if filename is None or not os.path.exists(filename):
return None
gadgets = eval(open(filename).read())
gadgets = safeeval.const(open(filename).read())
gadgets = {k - elf.load_addr + elf.address:v for k, v in gadgets.items()}
log.info_once('Loaded %s cached gadgets for %r', len(gadgets), elf.file.name)
return gadgets
@@ -1391,6 +1392,8 @@ class ROP(object):
if pop.match(insn):
regs.append(pop.match(insn).group(1))
sp_move += context.bytes
if 'sp' in insn:
sp_move += 9999999
elif add.match(insn):
arg = int(add.match(insn).group(1), 16)
sp_move += arg
@@ -1418,7 +1421,7 @@ class ROP(object):
if not set(['rsp', 'esp']) & set(regs):
self.pivots[sp_move] = addr
leave = self.search(regs=frame_regs, order='regs')
leave = self.search(regs=frame_regs, order='leav')
if leave and leave.regs != frame_regs:
leave = None
self.leave = leave
@@ -1451,7 +1454,7 @@ class ROP(object):
pointer is adjusted.
regs(list): Minimum list of registers which are popped off the
stack.
order(str): Either the string 'size' or 'regs'. Decides how to
order(str): Either the string 'size', 'leav' or 'regs'. Decides how to
order multiple gadgets the fulfill the requirements.
The search will try to minimize the number of bytes popped more than
@@ -1459,7 +1462,9 @@ class ROP(object):
the address.
If ``order == 'size'``, then gadgets are compared lexicographically
by ``(total_moves, total_regs, addr)``, otherwise by ``(total_regs, total_moves, addr)``.
by ``(total_moves, total_regs, addr)``, if ``order == 'regs'``,
then by ``(total_regs, total_moves, addr)``. ``order == 'leav'``
is specifically for ``leave`` insn.
Returns:
A :class:`.Gadget` object
@@ -1471,8 +1476,9 @@ class ROP(object):
# Search for an exact match, save the closest match
key = {
'size': lambda g: (g.move, len(g.regs), g.address),
'regs': lambda g: (len(g.regs), g.move, g.address)
}[order]
'regs': lambda g: (len(g.regs), g.move, g.address),
'leav': lambda g: ('leave' not in g.insns, len(g.regs), g.address)
}[order] # False is prior than True
try:
result = min(matches, key=key)
+5 -5
View File
@@ -185,7 +185,7 @@ registers = {
80: 'r13', 88: 'r14', 96: 'r15', 104: 'rdi', 112: 'rsi', 120: 'rbp', 128: 'rbx',
136: 'rdx', 144: 'rax', 152: 'rcx', 160: 'rsp', 168: 'rip', 176: 'eflags',
184: 'csgsfs', 192: 'err', 200: 'trapno', 208: 'oldmask', 216: 'cr2',
224: '&fpstate', 232: '__reserved', 240: 'sigmask'},
224: '&fpstate', 232: '__reserved', 296: 'sigmask'},
# Reference : http://lxr.free-electrons.com/source/arch/arm/include/uapi/asm/sigcontext.h#L15
'arm' : {0: 'uc_flags', 4: 'uc_link', 8: 'uc_stack.ss_sp', 12: 'uc_stack.ss_flags',
16: 'uc_stack.ss_size', 20: 'trap_no', 24: 'error_code', 28: 'oldmask', 32: 'r0',
@@ -270,15 +270,15 @@ class SigreturnFrame(dict):
>>> context.clear(arch='amd64')
>>> s = SigreturnFrame()
>>> unpack_many(bytes(s))
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0]
>>> assert len(s) == 248
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> assert len(s) == 304
>>> s.rax = 0xa
>>> s.rdi = 0x00601000
>>> s.rsi = 0x1000
>>> s.rdx = 0x7
>>> assert len(bytes(s)) == 248
>>> assert len(bytes(s)) == 304
>>> unpack_many(bytes(s))
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6295552, 4096, 0, 0, 7, 10, 0, 0, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6295552, 4096, 0, 0, 7, 10, 0, 0, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Crafting a SigreturnFrame that calls mprotect on i386
+39 -13
View File
@@ -170,21 +170,47 @@ tether = sys.modules[__name__]
# Create the module structure
shellcraft = module(__name__, '')
class LazyImporter:
def find_module(self, fullname, path=None):
if not fullname.startswith('pwnlib.shellcraft.'):
return None
parts = fullname.split('.')[2:]
cur = shellcraft
for part in parts:
cur = getattr(cur, part, None)
if not isinstance(cur, ModuleType):
if sys.version_info >= (3, 4):
import importlib.abc
import importlib.util
class LazyImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader):
def find_spec(self, fullname, path=None, target=None):
if not fullname.startswith('pwnlib.shellcraft.'):
return None
return self
parts = fullname.split('.')[2:]
cur = shellcraft
for part in parts:
cur = getattr(cur, part, None)
if not isinstance(cur, ModuleType):
return None
def load_module(self, fullname):
return sys.modules[fullname]
return importlib.util.spec_from_loader(fullname, self)
def create_module(self, spec):
# The submodule was already built and registered in sys.modules by
# the getattr walk in find_spec, so hand that object back instead
# of letting the import system make a fresh empty module.
return sys.modules.get(spec.name)
def exec_module(self, module):
pass
else:
class LazyImporter:
def find_module(self, fullname, path=None):
if not fullname.startswith('pwnlib.shellcraft.'):
return None
parts = fullname.split('.')[2:]
cur = shellcraft
for part in parts:
cur = getattr(cur, part, None)
if not isinstance(cur, ModuleType):
return None
return self
def load_module(self, fullname):
return sys.modules[fullname]
sys.meta_path.append(LazyImporter())
+6 -2
View File
@@ -767,9 +767,13 @@ class process(tube):
if IS_WINDOWS:
with self.countdown(timeout=timeout):
while self.timeout and self._read_queue.empty():
while self.timeout and self._read_queue.empty() and self._read_thread.is_alive():
time.sleep(0.01)
return not self._read_queue.empty()
if not self._read_queue.empty():
return True
if not self._read_thread.is_alive():
raise EOFError
return False
try:
if timeout is None:
+1 -1
View File
@@ -86,7 +86,7 @@ class remote(sock):
except socket.gaierror as e:
if e.errno != socket.EAI_NONAME:
raise
self.error('Could not resolve hostname: %r', host)
self.exception('Could not resolve hostname: %r', host)
if self.sock:
self.settimeout(self.timeout)
self.lhost, self.lport = self.sock.getsockname()[:2]
+11 -3
View File
@@ -692,6 +692,7 @@ class ssh(Timeout, Logger):
if user and auth_none and str(e) == "No authentication methods available":
self.client.get_transport().auth_none(user)
else:
self.close()
raise
self.transport = self.client.get_transport()
@@ -913,6 +914,13 @@ class ssh(Timeout, Logger):
>>> io.recvline()
b''
>>> io = s.process(['tty'], tty=True)
>>> io.recvline() # doctest: +ELLIPSIS
b'/dev/pts/...\n'
>>> io = s.process(['tty'], tty=False)
>>> io.recvline()
b'not a tty\n'
>>> # Testing that empty argv works
>>> io = s.process([], executable='sh')
>>> io.sendline(b'echo $0')
@@ -970,7 +978,7 @@ class ssh(Timeout, Logger):
script = 'echo PWNTOOLS; for py in python3 python2.7 python2 python; do test -x "$(command -v $py 2>&1)" && echo $py && exec $py -c %s check; done; echo 2' % sh_string(script)
with context.quiet:
python = ssh_process(self, script, tty=True, cwd=cwd, raw=True, level=self.level, timeout=timeout)
python = ssh_process(self, script, tty=tty, cwd=cwd, raw=raw, level=self.level, timeout=timeout)
try:
python.recvline_contains(b'PWNTOOLS') # Magic flag so that any sh/bash initialization errors are swallowed
@@ -1898,7 +1906,7 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
if not self.which('lsb_release'):
return
with self.process(['lsb_release', '-irs']) as io:
with self.process(['lsb_release', '-irs'], stderr='/dev/null') as io:
lsb_info = io.recvall().strip().decode()
self._platform_info['distro'], self._platform_info['distro_ver'] = lsb_info.split()
except Exception:
@@ -2081,7 +2089,7 @@ from ctypes import *; libc = CDLL('libc.so.6'); print(libc.getenv(%r))
Example:
>>> s = ssh("travis", "example.pwnme")
>>> s.ibt
>>> s.ibt # doctest: +SKIP
False
"""
if self._ibt is None:
+53 -32
View File
@@ -6,10 +6,12 @@ from __future__ import division
import collections
import copy
import marshal
import multiprocessing
import operator
import random
import time
import types
from itertools import *
from six.moves import map, filter, filterfalse, range, zip, zip_longest
@@ -27,7 +29,6 @@ __all__ = [
'group' ,
'iter_except' ,
'lexicographic' ,
'lookahead' ,
'nth' ,
'pad' ,
'pairwise' ,
@@ -683,37 +684,6 @@ def random_combination_with_replacement(iterable, r):
indices = sorted(random.randrange(n) for i in range(r))
return tuple(pool[i] for i in indices)
def lookahead(n, iterable):
"""lookahead(n, iterable) -> object
Inspects the upcoming element at index `n` without advancing the iterator.
Raises ``IndexError`` if `iterable` has too few elements.
Arguments:
n(int): Index of the element to return.
iterable: An iterable.
Returns:
The element in `iterable` at index `n`.
Examples:
>>> i = count()
>>> lookahead(4, i)
4
>>> next(i)
0
>>> i = count()
>>> nth(4, i)
4
>>> next(i)
5
>>> lookahead(4, i)
10
"""
for value in islice(copy.copy(iterable), n, None):
return value
raise IndexError(n)
def lexicographic(alphabet):
"""lexicographic(alphabet) -> iterator
@@ -877,6 +847,54 @@ def _mbruteforcewrap(func, alphabet, length, method, start, databag):
databag["result"] = res
class CustomPickler:
def __init__(self, *reduction):
self.reduction = reduction
def __reduce__(self):
return self.reduction
def __repr__(self):
return self.__class__.__name__ + repr(self.reduction)
def construct_func(*args):
return types.FunctionType(*args)
class PicklableFunc:
def __init__(self, f):
self.f = f
def __call__(self, *a, **kw):
return self.f(*a, **kw)
def __getattr__(self, a):
return getattr(self.f, a)
def __reduce__(self):
# Constructor types.FunctionType tries to be __main__.function;
# globs could likewise be <module>.__dict__. Neither
# type('FunctionType', (), {'__module__':'types'})
# nor
# def __reduce__(self): return "FunctionType"
# works because of checks for identity :(
globs = CustomPickler(vars, (CustomPickler(__import__, (self.f.__module__ or '__main__',)),))
return type(self), (CustomPickler(
construct_func, # ideally self.f.__class__
# but types.FunctionType tries to be __main__.function
(
CustomPickler(marshal.loads, (marshal.dumps(self.f.__code__),)),
globs,
self.f.__name__,
self.f.__defaults__,
self.f.__closure__,
self.f.__kwdefaults__,
),
self.f.__dict__,
),)
def mbruteforce(func, alphabet, length, method = 'upto', start = None, threads = None):
"""mbruteforce(func, alphabet, length, method = 'upto', start = None, threads = None)
@@ -914,6 +932,9 @@ def mbruteforce(func, alphabet, length, method = 'upto', start = None, threads =
(i2, N2) = start
totalchunks = threads * N2
if isinstance(func, types.FunctionType):
func = PicklableFunc(func)
for i in range(threads):
shareddata[i] = multiprocessing.Manager().dict()
shareddata[i]['result'] = None
+3 -2
View File
@@ -868,6 +868,7 @@ if sys.argv[-1] == 'check':
sys.stdout.write(str(os.getgid()) + "\n")
sys.stdout.write(str(suid) + "\n")
sys.stdout.write(str(sgid) + "\n")
sys.stdout.flush()
getattr(sys.stdout, 'buffer', sys.stdout).write(os.path.realpath(exe) + b'\x00')
sys.stdout.flush()
@@ -886,7 +887,7 @@ if not %(aslr)r:
ADDR_NO_RANDOMIZE = 0x0040000
ctypes.CDLL('libc.so.6').personality(ADDR_NO_RANDOMIZE)
resource.setrlimit(resource.RLIMIT_STACK, (-1, -1))
resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
# Attempt to dump ALL core file regions
try:
@@ -897,7 +898,7 @@ except Exception:
# Assume that the user would prefer to have core dumps.
try:
resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
except Exception:
pass
+7 -3
View File
@@ -4,8 +4,8 @@ _const_codes = [
'POP_TOP','ROT_TWO','ROT_THREE','ROT_FOUR','DUP_TOP',
'BUILD_LIST','BUILD_MAP', 'MAP_ADD', 'BUILD_TUPLE','BUILD_SET',
'BUILD_CONST_KEY_MAP', 'BUILD_STRING',
'LOAD_CONST','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP',
'LIST_TO_TUPLE', 'LIST_EXTEND', 'SET_UPDATE', 'DICT_UPDATE', 'DICT_MERGE',
'LOAD_CONST','LOAD_SMALL_INT','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP',
'LIST_TO_TUPLE', 'LIST_APPEND', 'LIST_EXTEND', 'SET_ADD', 'SET_UPDATE', 'DICT_UPDATE', 'DICT_MERGE',
'COPY', 'RESUME', 'RETURN_CONST'
]
@@ -29,7 +29,7 @@ def _get_opcodes(codeobj):
Extract the actual opcodes as a list from a code object
>>> c = compile("[1 + 2, (1,2)]", "", "eval")
>>> _get_opcodes(c) # doctest: +ELLIPSIS
>>> _get_opcodes(c) # doctest: +SKIP
[...100, 100, 103, 83]
"""
import dis
@@ -85,6 +85,10 @@ def const(expr):
Traceback (most recent call last):
...
ValueError: opcode BINARY_ADD not allowed
>>> const("[" + ",".join(str(i) for i in range(40)) + "]") == list(range(40))
True
>>> const("{" + ",".join(str(i) for i in range(40)) + "}") == set(range(40))
True
"""
c = test_expr(expr, _const_codes)
-1
View File
@@ -42,7 +42,6 @@ dependencies = [
"ropgadget>=5.3",
"pyserial>=2.7",
"requests>=2.0",
"pip>=6.0.8",
"pygments>=2.0",
"pysocks",
"python-dateutil",
+23 -10
View File
@@ -8,19 +8,26 @@ http {
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /dev/stdout cache_st;
# Use Docker's embedded DNS server to resolve hostnames at runtime
# instead of at startup.
resolver 127.0.0.11 8.8.8.8 ipv6=off valid=30s;
# Set default resolver for all upstreams to avoid startup-time DNS resolution
# This is a workaround for the occasional 'host not found in upstream' error
# in Docker environments where DNS may not be immediately available.
server {
listen 3000;
proxy_cache my_cache;
location / {
proxy_set_header Host debuginfod.elfutils.org;
set $upstream_host "debuginfod.elfutils.org";
proxy_set_header Host $upstream_host;
proxy_ssl_server_name on;
proxy_cache_revalidate on;
proxy_cache_key $scheme://$host$uri$is_args$query_string;
proxy_cache_valid 200 404 12w;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 http_429;
proxy_pass https://debuginfod.elfutils.org/;
proxy_pass https://$upstream_host$request_uri;
}
}
@@ -29,14 +36,15 @@ http {
proxy_cache my_cache;
location / {
proxy_set_header Host libc.rip;
set $upstream_host "libc.rip";
proxy_set_header Host $upstream_host;
proxy_ssl_server_name on;
proxy_cache_methods GET HEAD POST;
proxy_cache_revalidate on;
proxy_cache_key $scheme://$host$uri$is_args$query_string$request_body;
proxy_cache_valid 200 404 12w;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 http_429;
proxy_pass https://libc.rip/;
proxy_pass https://$upstream_host$request_uri;
}
}
@@ -45,12 +53,13 @@ http {
proxy_cache my_cache;
location / {
proxy_set_header Host archive.ubuntu.com;
set $upstream_host "archive.ubuntu.com";
proxy_set_header Host $upstream_host;
proxy_cache_revalidate on;
proxy_cache_key $scheme://$host$uri$is_args$query_string;
proxy_cache_valid 200 404 12w;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 http_429;
proxy_pass http://archive.ubuntu.com/;
proxy_pass http://$upstream_host$request_uri;
}
}
@@ -59,13 +68,15 @@ http {
proxy_cache my_cache;
location / {
proxy_set_header Host gitlab.com;
set $upstream_host "gitlab.com";
proxy_set_header Host $upstream_host;
proxy_ssl_server_name on;
proxy_cache_revalidate on;
proxy_cache_key $scheme://$host$uri$is_args$query_string;
proxy_cache_valid 200 404 12w;
proxy_ignore_headers Cache-Control Set-Cookie;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 http_429;
proxy_pass https://gitlab.com/;
proxy_pass https://$upstream_host$request_uri;
}
}
@@ -74,13 +85,15 @@ http {
proxy_cache my_cache;
location / {
proxy_set_header Host debuginfod.ubuntu.com;
set $upstream_host "debuginfod.ubuntu.com";
proxy_set_header Host $upstream_host;
proxy_ssl_server_name on;
proxy_cache_revalidate on;
proxy_cache_key $scheme://$host$uri$is_args$query_string;
proxy_cache_valid 200 404 12w;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 http_429;
proxy_pass https://debuginfod.ubuntu.com/;
proxy_pass https://$upstream_host$request_uri;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
docker run \
--name pwntools-test-nginx-cache \
--rm \
-v ./libcdb_nginx_cache.conf:/etc/nginx/nginx.conf:ro \
-p 3000:3000 \
-p 3001:3001 \
-p 3002:3002 \
-p 3003:3003 \
nginx
# curl -v localhost:3000/buildid/8e9fd827446c24067541ac5390e6f527fb5947bb/debuginfo
# curl -v localhost:3003/libcdb/libcdb/raw/master/hashes/build_id/fe136e485814fee2268cf19e5c124ed0f73f4400
# curl -v localhost:3003/libcdb/libcdb/raw/master/libc/libc6-i386-2.18-6/lib32/libc-2.18.so
# curl -v localhost:3003/libcdb/libcdb/raw/master/hashes/build_id/XX